mirror of
https://github.com/documenso/documenso.git
synced 2026-10-03 10:13:52 +10:00
feat: allow omitting document PDFs from emails
This commit is contained in:
@@ -569,6 +569,32 @@ const document = await response.json();
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
### Omit document PDFs from email
|
||||
|
||||
Set `meta.emailSettings.attachDocument` to `false` when creating or updating a document or template. Completion emails still go to the enabled recipients and owner, and retain their document links. This also omits PDFs from CC completion emails.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| --------- | ---- | ------- | ----------- |
|
||||
| `[meta.emailSettings.attachDocument]` | boolean | `true` | Attach completed document PDFs to emails. Set to `false` for links only. |
|
||||
|
||||
For example, update an envelope with this request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"envelopeId": "envelope_abc123",
|
||||
"meta": {
|
||||
"emailSettings": {
|
||||
"attachDocument": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When using a template with `/envelope/use`, pass the option in `override.emailSettings`. The document and template APIs, including API v1, accept the same boolean option in their email settings. Non-boolean values are rejected.
|
||||
|
||||
If you omit the email settings on creation, the document inherits the team's defaults, which can inherit from the organisation. Existing documents and templates retain their saved settings when those defaults change. If you provide an email settings object without `attachDocument`, attachments stay enabled for backward compatibility.
|
||||
|
||||
---
|
||||
|
||||
## Send Document
|
||||
|
||||
@@ -48,6 +48,12 @@ Or you can customize it before sending the document, by clicking on the **Docume
|
||||
|
||||

|
||||
|
||||
### Document PDF attachments
|
||||
|
||||
In **Document Settings > Notifications**, clear **Attach completed document PDFs to emails** to send completion emails with document links only. This applies to the owner and all recipients, including CCs. The email notifications remain enabled, and recipients can access the document through their links.
|
||||
|
||||
PDF attachments are enabled by default. Signing invitations and pending emails already include links without document PDFs. You can also set the attachment default for new documents and templates in your [email preferences](/docs/users/organisations/preferences/email).
|
||||
|
||||
### Reply-to email (optional)
|
||||
|
||||
Set a reply-to address so recipient replies go to a specific email instead of the default.
|
||||
|
||||
@@ -25,6 +25,12 @@ To access the preferences, navigate to either the organisation or teams settings
|
||||
| **Reply to email** | Email address used in the "Reply To" field in outgoing emails. Leave blank to inherit from the organisation. |
|
||||
| **Default Email Settings** | Controls which emails are sent to recipients during document signing (e.g. signing invitations, completion notifications). Teams can inherit from the organisation or override with custom settings. |
|
||||
|
||||
### Document PDF attachments
|
||||
|
||||
Under **Default Email Settings**, clear **Attach completed document PDFs to emails** to omit PDFs from completion emails to the owner and all recipients, including CCs. Emails still include document links. Attachments are enabled by default.
|
||||
|
||||
Teams inherit this preference with the other email settings. Select **Override organisation settings** to configure it for a team, or **Inherit from organisation** to restore inheritance. Changes apply to new documents and templates; existing documents and templates keep their saved settings. You can change the attachment setting for an individual document or template in **Document Settings > Notifications**.
|
||||
|
||||
## See Also
|
||||
|
||||
- [Email Domains](/docs/users/organisations/email-domains) - Send emails from your own domain
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { ReactNode } from 'react';
|
||||
import { DetailsCard, DetailsValue } from '~/components/general/admin-details';
|
||||
|
||||
const EMAIL_SETTINGS_LABELS: Record<keyof TDocumentEmailSettings, MessageDescriptor> = {
|
||||
attachDocument: msg`Attach completed document PDFs`,
|
||||
recipientSigningRequest: msg`Recipient signing request`,
|
||||
recipientRemoved: msg`Recipient removed`,
|
||||
recipientSigned: msg`Recipient signed`,
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function OrganisationSettingsGeneral() {
|
||||
emailId,
|
||||
emailReplyTo: emailReplyTo || null,
|
||||
// emailReplyToName,
|
||||
emailDocumentSettings,
|
||||
emailDocumentSettings: emailDocumentSettings ?? undefined,
|
||||
includeSenderDetails: includeSenderDetails ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
ZRecipientActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
||||
import { ZFieldMetaPrefillFieldsSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { ZRecipientSigningOrderSchema } from '@documenso/lib/types/recipient';
|
||||
@@ -172,7 +172,7 @@ export const ZCreateDocumentMutationSchema = z.object({
|
||||
uploadSignatureEnabled: z.boolean().optional().default(true),
|
||||
drawSignatureEnabled: z.boolean().optional().default(true),
|
||||
distributionMethod: z.nativeEnum(DocumentDistributionMethod).optional(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
|
||||
})
|
||||
.partial()
|
||||
.optional()
|
||||
@@ -341,7 +341,7 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
||||
typedSignatureEnabled: z.boolean(),
|
||||
uploadSignatureEnabled: z.boolean(),
|
||||
drawSignatureEnabled: z.boolean(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema,
|
||||
emailSettings: ZDocumentEmailSettingsInputSchema,
|
||||
})
|
||||
.partial()
|
||||
.optional(),
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
addEnvelopeItemPdf,
|
||||
getEnvelopeEditorSettingsTrigger,
|
||||
openDocumentEnvelopeEditor,
|
||||
openEmbeddedEnvelopeEditor,
|
||||
openTemplateEnvelopeEditor,
|
||||
persistEmbeddedEnvelope,
|
||||
setRecipientEmail,
|
||||
type TEnvelopeEditorSurface,
|
||||
} from '../fixtures/envelope-editor';
|
||||
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
||||
|
||||
const runAttachmentSettingsFlow = async (surface: TEnvelopeEditorSurface) => {
|
||||
const { root, isEmbedded } = surface;
|
||||
const externalId = `e2e-email-attachments-${nanoid()}`;
|
||||
|
||||
if (isEmbedded && !surface.envelopeId) {
|
||||
await addEnvelopeItemPdf(root);
|
||||
await setRecipientEmail(root, 0, 'signer@example.com');
|
||||
}
|
||||
|
||||
for (const attachDocument of [false, true, false]) {
|
||||
await getEnvelopeEditorSettingsTrigger(root).click();
|
||||
await root.locator('input[name="externalId"]').fill(externalId);
|
||||
await root.getByRole('button', { name: 'Notifications' }).click();
|
||||
const attachmentControl = root.getByRole('checkbox', { name: 'Attach completed document PDFs to emails' });
|
||||
await expect(attachmentControl).toBeChecked({ checked: !attachDocument });
|
||||
await attachmentControl.setChecked(attachDocument);
|
||||
await expect(root.locator('#documentCompleted')).toBeChecked();
|
||||
await expect(root.locator('#ownerDocumentCompleted')).toBeChecked();
|
||||
await root.getByRole('button', { name: 'Update', exact: true }).click();
|
||||
|
||||
if (!isEmbedded) {
|
||||
await expectToastTextToBeVisible(root, 'Envelope updated');
|
||||
const meta = await prisma.envelope.findUniqueOrThrow({
|
||||
where: { id: surface.envelopeId },
|
||||
include: { documentMeta: true },
|
||||
});
|
||||
expect(meta.documentMeta.emailSettings).toMatchObject({ attachDocument });
|
||||
await root.reload();
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmbedded) {
|
||||
await persistEmbeddedEnvelope(surface);
|
||||
}
|
||||
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: { externalId, userId: surface.userId, teamId: surface.teamId, type: surface.envelopeType },
|
||||
include: { documentMeta: true },
|
||||
});
|
||||
expect(envelope.documentMeta.emailSettings).toMatchObject({
|
||||
attachDocument: false,
|
||||
documentCompleted: true,
|
||||
ownerDocumentCompleted: true,
|
||||
});
|
||||
};
|
||||
|
||||
test.describe('document editor', () => {
|
||||
test('save email attachment opt-out and toggle back', async ({ page }) => {
|
||||
await runAttachmentSettingsFlow(await openDocumentEnvelopeEditor(page));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('template editor', () => {
|
||||
test('save email attachment opt-out and toggle back', async ({ page }) => {
|
||||
await runAttachmentSettingsFlow(await openTemplateEnvelopeEditor(page));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('embedded create', () => {
|
||||
test('save email attachment opt-out and toggle back', async ({ page }) => {
|
||||
await runAttachmentSettingsFlow(await openEmbeddedEnvelopeEditor(page, { envelopeType: 'DOCUMENT' }));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('embedded edit', () => {
|
||||
test('save email attachment opt-out and toggle back', async ({ page }) => {
|
||||
await runAttachmentSettingsFlow(await openEmbeddedEnvelopeEditor(page, { envelopeType: 'TEMPLATE', mode: 'edit' }));
|
||||
});
|
||||
});
|
||||
@@ -215,6 +215,8 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
|
||||
await page.getByRole('textbox', { name: 'Reply to email' }).click();
|
||||
await page.getByRole('textbox', { name: 'Reply to email' }).fill('organisation@documenso.com');
|
||||
|
||||
await page.getByRole('checkbox', { name: 'Attach completed document PDFs to emails' }).uncheck();
|
||||
|
||||
// Update email document settings by enabling/disabling some checkboxes
|
||||
await page.getByRole('checkbox', { name: 'Email the owner when a recipient signs' }).uncheck();
|
||||
await page.getByRole('checkbox', { name: 'Email the signer if the document is still pending' }).uncheck();
|
||||
@@ -230,6 +232,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
|
||||
// Check that the team settings have inherited these values.
|
||||
expect(teamSettings.emailReplyTo).toEqual('organisation@documenso.com');
|
||||
expect(teamSettings.emailDocumentSettings).toEqual({
|
||||
attachDocument: false,
|
||||
recipientSigningRequest: true,
|
||||
recipientRemoved: true,
|
||||
recipientSigned: false, // unchecked
|
||||
@@ -273,6 +276,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
|
||||
// Check that the team settings have overridden the organisation values.
|
||||
expect(updatedTeamSettings.emailReplyTo).toEqual('team@example.com');
|
||||
expect(updatedTeamSettings.emailDocumentSettings).toEqual({
|
||||
attachDocument: true,
|
||||
recipientSigned: true,
|
||||
recipientSigningRequest: false,
|
||||
recipientRemoved: true,
|
||||
@@ -295,6 +299,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
|
||||
|
||||
expect(teamOverrideDocumentMeta.emailReplyTo).toEqual('team@example.com');
|
||||
expect(teamOverrideDocumentMeta.emailSettings).toEqual({
|
||||
attachDocument: true,
|
||||
recipientSigned: true,
|
||||
recipientSigningRequest: false,
|
||||
recipientRemoved: true,
|
||||
@@ -322,6 +327,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
|
||||
// Check that the team settings now inherit from organisation again.
|
||||
expect(inheritedTeamSettings.emailReplyTo).toEqual('organisation@documenso.com');
|
||||
expect(inheritedTeamSettings.emailDocumentSettings).toEqual({
|
||||
attachDocument: false,
|
||||
recipientSigningRequest: true,
|
||||
recipientRemoved: true,
|
||||
recipientSigned: false,
|
||||
@@ -344,6 +350,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
|
||||
|
||||
expect(documentMeta.emailReplyTo).toEqual('organisation@documenso.com');
|
||||
expect(documentMeta.emailSettings).toEqual({
|
||||
attachDocument: false,
|
||||
recipientSigningRequest: true,
|
||||
recipientRemoved: true,
|
||||
recipientSigned: false,
|
||||
@@ -354,4 +361,22 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
|
||||
ownerDocumentCompleted: true,
|
||||
ownerDocumentCreated: true,
|
||||
});
|
||||
|
||||
// Re-enable attachments and verify that inherited defaults change only for new documents.
|
||||
await page.goto(`/o/${organisation.url}/settings/email`);
|
||||
const attachmentControl = page.getByRole('checkbox', { name: 'Attach completed document PDFs to emails' });
|
||||
await expect(attachmentControl).not.toBeChecked();
|
||||
await attachmentControl.check();
|
||||
await page.getByRole('button', { name: 'Save changes' }).first().click();
|
||||
await expect(page.getByText('Your email preferences have been updated').first()).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(attachmentControl).toBeChecked();
|
||||
|
||||
const restoredSettings = await getTeamSettings({ teamId: team.id });
|
||||
expect(restoredSettings.emailDocumentSettings.attachDocument).toBe(true);
|
||||
const newDocument = await seedTeamDocumentWithMeta(team);
|
||||
const newMeta = await prisma.documentMeta.findUniqueOrThrow({ where: { id: newDocument.documentMetaId } });
|
||||
expect(newMeta.emailSettings?.attachDocument).toBe(true);
|
||||
const existingMeta = await prisma.documentMeta.findUniqueOrThrow({ where: { id: document.documentMetaId } });
|
||||
expect(existingMeta.emailSettings?.attachDocument).toBe(false);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { DocumentDistributionMethod, DocumentSource, RecipientRole } from '@prisma/client';
|
||||
import type { ReactElement } from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { DEFAULT_DOCUMENT_EMAIL_SETTINGS } from '../../../types/document-email';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import { run } from './send-document-completed-emails.handler';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
findEnvelope: vi.fn(),
|
||||
createAuditLog: vi.fn(),
|
||||
findEnvelopeItems: vi.fn(),
|
||||
getEmailContext: vi.fn(),
|
||||
getFile: vi.fn(),
|
||||
sendMail: vi.fn(),
|
||||
checkLimits: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@documenso/prisma', () => ({
|
||||
prisma: {
|
||||
envelope: { findUnique: mocks.findEnvelope },
|
||||
envelopeItem: { findMany: mocks.findEnvelopeItems },
|
||||
documentAuditLog: { create: mocks.createAuditLog },
|
||||
},
|
||||
}));
|
||||
vi.mock('@documenso/email/templates/document-completed', () => ({
|
||||
DocumentCompletedEmailTemplate: () => null,
|
||||
}));
|
||||
vi.mock('../../../client-only/providers/i18n-server', () => ({
|
||||
getI18nInstance: async () => ({ _: () => 'Signing Complete!' }),
|
||||
}));
|
||||
vi.mock('../../../constants/app', () => ({ NEXT_PUBLIC_WEBAPP_URL: () => 'http://localhost:3000' }));
|
||||
vi.mock('../../../server-only/email/get-email-context', () => ({ getEmailContext: mocks.getEmailContext }));
|
||||
vi.mock('../../../server-only/rate-limit/assert-organisation-rates-and-limits', () => ({
|
||||
assertOrganisationRatesAndLimits: mocks.checkLimits,
|
||||
}));
|
||||
vi.mock('../../../universal/upload/get-file.server', () => ({ getFileServerSide: mocks.getFile }));
|
||||
vi.mock('../../../utils/document-audit-logs', () => ({ createDocumentAuditLogData: (data: unknown) => data }));
|
||||
vi.mock('../../../utils/teams', () => ({ formatDocumentsPath: () => '/t/test-team/documents' }));
|
||||
vi.mock('../../../utils/render-email-with-i18n', () => ({
|
||||
renderEmailWithI18N: async (template: ReactElement<{ downloadLink: string }>) => template.props.downloadLink,
|
||||
}));
|
||||
|
||||
const createEnvelope = (distributionMethod: DocumentDistributionMethod = DocumentDistributionMethod.EMAIL) => ({
|
||||
id: 'envelope_test',
|
||||
secondaryId: 'document_1',
|
||||
teamId: 1,
|
||||
internalVersion: 2,
|
||||
title: 'Confidential document',
|
||||
source: DocumentSource.DOCUMENT,
|
||||
team: { id: 1, url: 'test-team' },
|
||||
user: { id: 1, name: 'Owner', email: 'owner@example.com', disabled: false },
|
||||
documentMeta: {
|
||||
distributionMethod,
|
||||
emailSettings: { ...DEFAULT_DOCUMENT_EMAIL_SETTINGS },
|
||||
},
|
||||
envelopeItems: [
|
||||
{ title: 'First document', documentData: { id: 'file_1', type: 'S3', data: 'file_1' } },
|
||||
{ title: 'Second document', documentData: { id: 'file_2', type: 'BYTES', data: 'file_2' } },
|
||||
],
|
||||
recipients: [
|
||||
{ id: 2, name: 'Signer', email: 'signer@example.com', role: RecipientRole.SIGNER, token: 'signer-token' },
|
||||
{ id: 3, name: 'CC', email: 'cc@example.com', role: RecipientRole.CC, token: 'cc-token' },
|
||||
],
|
||||
});
|
||||
|
||||
let envelope = createEnvelope();
|
||||
const io = { logger: { warn: vi.fn() } } as unknown as JobRunIO;
|
||||
const runJob = () => run({ payload: { envelopeId: envelope.id }, io });
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
envelope = createEnvelope();
|
||||
mocks.findEnvelope.mockImplementation(async () => envelope);
|
||||
mocks.findEnvelopeItems.mockImplementation(async () => envelope.envelopeItems);
|
||||
mocks.getFile.mockResolvedValue(new Uint8Array([1, 2, 3]));
|
||||
mocks.getEmailContext.mockResolvedValue({
|
||||
branding: {},
|
||||
emailLanguage: 'en',
|
||||
senderEmail: 'sender@example.com',
|
||||
replyToEmail: 'reply@example.com',
|
||||
organisationId: 'org_test',
|
||||
claims: {},
|
||||
emailsDisabled: false,
|
||||
emailTransport: { sendMail: mocks.sendMail },
|
||||
});
|
||||
});
|
||||
|
||||
describe('completion email document attachments', () => {
|
||||
it('keeps email content, links, audit logs, and CC metering when PDFs are disabled', async () => {
|
||||
envelope.documentMeta.emailSettings.attachDocument = false;
|
||||
// Loading a PDF would fail: link-only email must still succeed.
|
||||
mocks.getFile.mockRejectedValue(new Error('PDF storage unavailable'));
|
||||
|
||||
await runJob();
|
||||
|
||||
expect(mocks.findEnvelopeItems).not.toHaveBeenCalled();
|
||||
expect(mocks.getFile).not.toHaveBeenCalled();
|
||||
expect(mocks.sendMail).toHaveBeenCalledTimes(3);
|
||||
const emails = mocks.sendMail.mock.calls.map(([email]) => email);
|
||||
expect(emails.map((email) => email.to[0].address)).toEqual([
|
||||
'owner@example.com',
|
||||
'signer@example.com',
|
||||
'cc@example.com',
|
||||
]);
|
||||
for (const email of emails) {
|
||||
expect(email).toMatchObject({ attachments: [], subject: 'Signing Complete!' });
|
||||
expect(email.html).toBe(email.text);
|
||||
}
|
||||
expect(emails[0].html).toBe('http://localhost:3000/t/test-team/documents/envelope_test');
|
||||
expect(emails[1].html).toBe('http://localhost:3000/sign/signer-token/complete');
|
||||
expect(emails[2].html).toBe('http://localhost:3000/sign/cc-token/complete');
|
||||
expect(mocks.createAuditLog).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.checkLimits).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each([true, undefined])('attaches all PDFs by default and when enabled (%s)', async (attachDocument) => {
|
||||
mocks.findEnvelope.mockResolvedValue({
|
||||
...envelope,
|
||||
documentMeta: { ...envelope.documentMeta, emailSettings: { attachDocument } },
|
||||
});
|
||||
|
||||
await runJob();
|
||||
|
||||
expect(mocks.getFile).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.sendMail).toHaveBeenCalledTimes(3);
|
||||
for (const [email] of mocks.sendMail.mock.calls) {
|
||||
expect(email.attachments).toEqual([
|
||||
{ filename: 'First document.pdf', content: Buffer.from([1, 2, 3]), contentType: 'application/pdf' },
|
||||
{ filename: 'Second document.pdf', content: Buffer.from([1, 2, 3]), contentType: 'application/pdf' },
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('retains legacy single-document filenames', async () => {
|
||||
envelope.internalVersion = 1;
|
||||
envelope.envelopeItems = [envelope.envelopeItems[0]];
|
||||
await runJob();
|
||||
expect(mocks.sendMail.mock.calls[0][0].attachments[0].filename).toBe('Confidential document.pdf');
|
||||
});
|
||||
|
||||
it('sends link-only owner emails when recipient emails are disabled', async () => {
|
||||
envelope.documentMeta.emailSettings.attachDocument = false;
|
||||
envelope.documentMeta.emailSettings.documentCompleted = false;
|
||||
await runJob();
|
||||
expect(mocks.findEnvelopeItems).not.toHaveBeenCalled();
|
||||
expect(mocks.getFile).not.toHaveBeenCalled();
|
||||
expect(mocks.sendMail).toHaveBeenCalledOnce();
|
||||
expect(mocks.sendMail.mock.calls[0][0].to[0].address).toBe('owner@example.com');
|
||||
});
|
||||
|
||||
it('sends link-only recipient emails when owner emails are disabled', async () => {
|
||||
envelope.documentMeta.emailSettings.attachDocument = false;
|
||||
envelope.documentMeta.emailSettings.ownerDocumentCompleted = false;
|
||||
await runJob();
|
||||
expect(mocks.findEnvelopeItems).not.toHaveBeenCalled();
|
||||
expect(mocks.getFile).not.toHaveBeenCalled();
|
||||
expect(mocks.sendMail).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('sends a single link-only completion email when the owner is also a recipient', async () => {
|
||||
envelope.documentMeta.emailSettings.attachDocument = false;
|
||||
envelope.recipients[0].email = envelope.user.email;
|
||||
await runJob();
|
||||
expect(mocks.sendMail).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.sendMail.mock.calls[0][0]).toMatchObject({
|
||||
to: [{ address: 'owner@example.com' }],
|
||||
attachments: [],
|
||||
html: 'http://localhost:3000/t/test-team/documents/envelope_test',
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves owner-only link emails when distribution is NONE', async () => {
|
||||
envelope.documentMeta.distributionMethod = DocumentDistributionMethod.NONE;
|
||||
envelope.documentMeta.emailSettings.attachDocument = false;
|
||||
await runJob();
|
||||
expect(mocks.findEnvelopeItems).not.toHaveBeenCalled();
|
||||
expect(mocks.getFile).not.toHaveBeenCalled();
|
||||
expect(mocks.sendMail).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('does not load PDFs or send mail when both completion emails are disabled', async () => {
|
||||
envelope.documentMeta.emailSettings.documentCompleted = false;
|
||||
envelope.documentMeta.emailSettings.ownerDocumentCompleted = false;
|
||||
await runJob();
|
||||
expect(mocks.findEnvelopeItems).not.toHaveBeenCalled();
|
||||
expect(mocks.getFile).not.toHaveBeenCalled();
|
||||
expect(mocks.sendMail).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -26,17 +26,6 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmai
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: unsafeBuildEnvelopeIdQuery({ type: 'envelopeId', id: envelopeId }, EnvelopeType.DOCUMENT),
|
||||
include: {
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: {
|
||||
select: {
|
||||
type: true,
|
||||
id: true,
|
||||
data: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
user: {
|
||||
@@ -83,15 +72,39 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmai
|
||||
|
||||
const { user: owner } = envelope;
|
||||
|
||||
const emailSettings = extractDerivedDocumentEmailSettings(envelope.documentMeta);
|
||||
const isDocumentCompletedEmailEnabled = emailSettings.documentCompleted;
|
||||
const isOwnerDocumentCompletedEmailEnabled = emailSettings.ownerDocumentCompleted;
|
||||
|
||||
if (!isDocumentCompletedEmailEnabled && !isOwnerDocumentCompletedEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch PDF data only when attachments are enabled. Database-backed PDFs can
|
||||
// contain the entire file in documentData.data.
|
||||
const envelopeItems = emailSettings.attachDocument
|
||||
? await prisma.envelopeItem.findMany({
|
||||
where: { envelopeId: envelope.id },
|
||||
include: {
|
||||
documentData: {
|
||||
select: {
|
||||
type: true,
|
||||
data: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
const completedDocumentEmailAttachments = await Promise.all(
|
||||
envelope.envelopeItems.map(async (envelopeItem) => {
|
||||
envelopeItems.map(async (envelopeItem) => {
|
||||
const file = await getFileServerSide(envelopeItem.documentData);
|
||||
|
||||
// Use the envelope title for version 1, and the envelope item title for version 2.
|
||||
const fileNameToUse = envelope.internalVersion === 1 ? envelope.title : envelopeItem.title + '.pdf';
|
||||
const fileNameToUse = envelope.internalVersion === 1 ? envelope.title : `${envelopeItem.title}.pdf`;
|
||||
|
||||
return {
|
||||
filename: fileNameToUse.endsWith('.pdf') ? fileNameToUse : fileNameToUse + '.pdf',
|
||||
filename: fileNameToUse.endsWith('.pdf') ? fileNameToUse : `${fileNameToUse}.pdf`,
|
||||
content: Buffer.from(file),
|
||||
contentType: 'application/pdf',
|
||||
};
|
||||
@@ -108,10 +121,6 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmai
|
||||
documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${envelope.team.url}/documents/${envelope.id}`;
|
||||
}
|
||||
|
||||
const emailSettings = extractDerivedDocumentEmailSettings(envelope.documentMeta);
|
||||
const isDocumentCompletedEmailEnabled = emailSettings.documentCompleted;
|
||||
const isOwnerDocumentCompletedEmailEnabled = emailSettings.ownerDocumentCompleted;
|
||||
|
||||
// Send email to document owner if:
|
||||
// 1. Owner document completed emails are enabled AND
|
||||
// 2. Either:
|
||||
|
||||
@@ -437,6 +437,7 @@ export const generateSampleWebhookPayload = (event: WebhookTriggerEvents, webhoo
|
||||
dateFormat: 'yyyy-MM-dd hh:mm a',
|
||||
redirectUrl: '',
|
||||
emailSettings: {
|
||||
attachDocument: true,
|
||||
documentDeleted: true,
|
||||
documentPending: true,
|
||||
recipientSigned: true,
|
||||
|
||||
@@ -15322,3 +15322,15 @@ msgstr "Ihr Verifizierungscode:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "your-domain.com another-domain.com"
|
||||
|
||||
#: apps/remix/app/components/general/admin-global-settings-section.tsx
|
||||
msgid "Attach completed document PDFs"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Attach completed document PDFs to emails"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only."
|
||||
msgstr ""
|
||||
|
||||
@@ -15317,3 +15317,15 @@ msgstr "Your verification code:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "your-domain.com another-domain.com"
|
||||
|
||||
#: apps/remix/app/components/general/admin-global-settings-section.tsx
|
||||
msgid "Attach completed document PDFs"
|
||||
msgstr "Attach completed document PDFs"
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Attach completed document PDFs to emails"
|
||||
msgstr "Attach completed document PDFs to emails"
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only."
|
||||
msgstr "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only."
|
||||
|
||||
@@ -15322,3 +15322,15 @@ msgstr "Su código de verificación:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "su-dominio.com otro-dominio.com"
|
||||
|
||||
#: apps/remix/app/components/general/admin-global-settings-section.tsx
|
||||
msgid "Attach completed document PDFs"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Attach completed document PDFs to emails"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only."
|
||||
msgstr ""
|
||||
|
||||
@@ -15322,3 +15322,15 @@ msgstr "Votre code de vérification :"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "your-domain.com another-domain.com"
|
||||
|
||||
#: apps/remix/app/components/general/admin-global-settings-section.tsx
|
||||
msgid "Attach completed document PDFs"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Attach completed document PDFs to emails"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only."
|
||||
msgstr ""
|
||||
|
||||
@@ -15322,3 +15322,15 @@ msgstr "Il tuo codice di verifica:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "tuo-dominio.com altro-dominio.com"
|
||||
|
||||
#: apps/remix/app/components/general/admin-global-settings-section.tsx
|
||||
msgid "Attach completed document PDFs"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Attach completed document PDFs to emails"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only."
|
||||
msgstr ""
|
||||
|
||||
@@ -15322,3 +15322,15 @@ msgstr "認証コード:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "your-domain.com another-domain.com"
|
||||
|
||||
#: apps/remix/app/components/general/admin-global-settings-section.tsx
|
||||
msgid "Attach completed document PDFs"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Attach completed document PDFs to emails"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only."
|
||||
msgstr ""
|
||||
|
||||
@@ -15322,3 +15322,15 @@ msgstr "인증 코드:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "your-domain.com another-domain.com"
|
||||
|
||||
#: apps/remix/app/components/general/admin-global-settings-section.tsx
|
||||
msgid "Attach completed document PDFs"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Attach completed document PDFs to emails"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only."
|
||||
msgstr ""
|
||||
|
||||
@@ -15322,3 +15322,15 @@ msgstr "Uw verificatiecode:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "your-domain.com another-domain.com"
|
||||
|
||||
#: apps/remix/app/components/general/admin-global-settings-section.tsx
|
||||
msgid "Attach completed document PDFs"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Attach completed document PDFs to emails"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only."
|
||||
msgstr ""
|
||||
|
||||
@@ -15359,3 +15359,14 @@ msgstr "Powiększ"
|
||||
msgid "Zoom out"
|
||||
msgstr "Pomniejsz"
|
||||
|
||||
#: apps/remix/app/components/general/admin-global-settings-section.tsx
|
||||
msgid "Attach completed document PDFs"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Attach completed document PDFs to emails"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only."
|
||||
msgstr ""
|
||||
|
||||
@@ -15317,3 +15317,15 @@ msgstr "Seu código de verificação:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "seu-dominio.com outro-dominio.com"
|
||||
|
||||
#: apps/remix/app/components/general/admin-global-settings-section.tsx
|
||||
msgid "Attach completed document PDFs"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Attach completed document PDFs to emails"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only."
|
||||
msgstr ""
|
||||
|
||||
@@ -15322,3 +15322,15 @@ msgstr "您的验证码:"
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "your-domain.com another-domain.com"
|
||||
msgstr "your-domain.com another-domain.com"
|
||||
|
||||
#: apps/remix/app/components/general/admin-global-settings-section.tsx
|
||||
msgid "Attach completed document PDFs"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Attach completed document PDFs to emails"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/document/document-email-checkboxes.tsx
|
||||
msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only."
|
||||
msgstr ""
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ZCreateDocumentMutationSchema, ZGenerateDocumentFromTemplateMutationSchema } from '../../api/v1/schema';
|
||||
import { ZCreateDocumentPayloadSchema } from '../../trpc/server/document-router/create-document.types';
|
||||
import { ZCreateEnvelopePayloadSchema } from '../../trpc/server/envelope-router/create-envelope.types';
|
||||
import { ZUpdateEnvelopeRequestSchema } from '../../trpc/server/envelope-router/update-envelope.types';
|
||||
import { ZUseEnvelopePayloadSchema } from '../../trpc/server/envelope-router/use-envelope.types';
|
||||
import { ZUpdateOrganisationSettingsRequestSchema } from '../../trpc/server/organisation-router/update-organisation-settings.types';
|
||||
import { ZUpdateTeamSettingsRequestSchema } from '../../trpc/server/team-router/update-team-settings.types';
|
||||
import {
|
||||
ZCreateDocumentFromTemplateRequestSchema,
|
||||
ZCreateTemplateV2RequestSchema,
|
||||
} from '../../trpc/server/template-router/schema';
|
||||
|
||||
const requestCases = [
|
||||
{
|
||||
name: 'v1 document creation',
|
||||
schema: ZCreateDocumentMutationSchema,
|
||||
body: { title: 'Document', recipients: [] },
|
||||
field: 'meta',
|
||||
},
|
||||
{
|
||||
name: 'v1 template use',
|
||||
schema: ZGenerateDocumentFromTemplateMutationSchema,
|
||||
body: { recipients: [] },
|
||||
field: 'meta',
|
||||
},
|
||||
{ name: 'document creation', schema: ZCreateDocumentPayloadSchema, body: { title: 'Document' }, field: 'meta' },
|
||||
{
|
||||
name: 'envelope creation',
|
||||
schema: ZCreateEnvelopePayloadSchema,
|
||||
body: { title: 'Document', type: 'DOCUMENT' },
|
||||
field: 'meta',
|
||||
},
|
||||
{
|
||||
name: 'envelope update',
|
||||
schema: ZUpdateEnvelopeRequestSchema,
|
||||
body: { envelopeId: 'envelope_test' },
|
||||
field: 'meta',
|
||||
},
|
||||
{
|
||||
name: 'envelope template use',
|
||||
schema: ZUseEnvelopePayloadSchema,
|
||||
body: { envelopeId: 'envelope_test' },
|
||||
field: 'override',
|
||||
},
|
||||
{ name: 'template creation', schema: ZCreateTemplateV2RequestSchema, body: { title: 'Template' }, field: 'meta' },
|
||||
{
|
||||
name: 'document from template',
|
||||
schema: ZCreateDocumentFromTemplateRequestSchema,
|
||||
body: { templateId: 1, recipients: [] },
|
||||
field: 'override',
|
||||
},
|
||||
] as const;
|
||||
|
||||
describe.each(requestCases)('$name email settings API', ({ schema, body, field }) => {
|
||||
it.each([true, false])('accepts and retains attachDocument=%s', (attachDocument) => {
|
||||
const request = { ...body, [field]: { emailSettings: { attachDocument } } };
|
||||
const result = schema.parse(request);
|
||||
expect(result).toMatchObject({ [field]: { emailSettings: { attachDocument } } });
|
||||
});
|
||||
|
||||
it('rejects non-boolean attachment controls instead of enabling attachments', () => {
|
||||
const request = { ...body, [field]: { emailSettings: { attachDocument: 'false' } } };
|
||||
expect(schema.safeParse(request).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default email settings API', () => {
|
||||
it.each([true, false])('retains organisation and team attachment defaults (%s)', (attachDocument) => {
|
||||
expect(
|
||||
ZUpdateOrganisationSettingsRequestSchema.parse({
|
||||
organisationId: 'org_test',
|
||||
data: { emailDocumentSettings: { attachDocument } },
|
||||
}),
|
||||
).toMatchObject({ data: { emailDocumentSettings: { attachDocument } } });
|
||||
expect(
|
||||
ZUpdateTeamSettingsRequestSchema.parse({ teamId: 1, data: { emailDocumentSettings: { attachDocument } } }),
|
||||
).toMatchObject({ data: { emailDocumentSettings: { attachDocument } } });
|
||||
});
|
||||
|
||||
it('rejects malformed organisation and team defaults', () => {
|
||||
expect(
|
||||
ZUpdateOrganisationSettingsRequestSchema.safeParse({
|
||||
organisationId: 'org_test',
|
||||
data: { emailDocumentSettings: { attachDocument: 'false' } },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
ZUpdateTeamSettingsRequestSchema.safeParse({
|
||||
teamId: 1,
|
||||
data: { emailDocumentSettings: { attachDocument: 'false' } },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('retains null as the team inheritance control', () => {
|
||||
expect(ZUpdateTeamSettingsRequestSchema.parse({ teamId: 1, data: { emailDocumentSettings: null } })).toMatchObject({
|
||||
data: { emailDocumentSettings: null },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { DocumentDistributionMethod, type DocumentMeta } from '@prisma/client';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { extractDerivedDocumentMeta } from '../utils/document';
|
||||
import { generateDefaultOrganisationSettings } from '../utils/organisations';
|
||||
import { extractDerivedTeamSettings, generateDefaultTeamSettings } from '../utils/teams';
|
||||
import {
|
||||
DEFAULT_DOCUMENT_EMAIL_SETTINGS,
|
||||
extractDerivedDocumentEmailSettings,
|
||||
ZDocumentEmailSettingsInputSchema,
|
||||
ZDocumentEmailSettingsSchema,
|
||||
} from './document-email';
|
||||
import { ZDocumentMetaCreateSchema, ZDocumentMetaUpdateSchema } from './document-meta';
|
||||
|
||||
describe('document email attachments', () => {
|
||||
it('keeps attachments enabled for absent and legacy settings', () => {
|
||||
expect(extractDerivedDocumentEmailSettings().attachDocument).toBe(true);
|
||||
expect(ZDocumentEmailSettingsSchema.parse(null).attachDocument).toBe(true);
|
||||
expect(ZDocumentEmailSettingsInputSchema.parse({ documentCompleted: false })).toMatchObject({
|
||||
attachDocument: true,
|
||||
documentCompleted: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('disables attachments independently of notification events', () => {
|
||||
expect(ZDocumentEmailSettingsInputSchema.parse({ attachDocument: false })).toEqual({
|
||||
...DEFAULT_DOCUMENT_EMAIL_SETTINGS,
|
||||
attachDocument: false,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['false', 0, null, [], {}])('rejects invalid attachment values: %j', (attachDocument) => {
|
||||
expect(ZDocumentEmailSettingsInputSchema.safeParse({ attachDocument }).success).toBe(false);
|
||||
expect(ZDocumentMetaCreateSchema.safeParse({ emailSettings: { attachDocument } }).success).toBe(false);
|
||||
expect(ZDocumentMetaUpdateSchema.safeParse({ emailSettings: { attachDocument } }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('does not replace an explicit opt-out with defaults when another input is invalid', () => {
|
||||
expect(
|
||||
ZDocumentMetaCreateSchema.safeParse({
|
||||
emailSettings: { attachDocument: false, documentCompleted: 'true' },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves the owner attachment setting when email distribution is disabled', () => {
|
||||
const settings = extractDerivedDocumentEmailSettings({
|
||||
distributionMethod: DocumentDistributionMethod.NONE,
|
||||
emailSettings: { ...DEFAULT_DOCUMENT_EMAIL_SETTINGS, attachDocument: false },
|
||||
} as DocumentMeta);
|
||||
|
||||
expect(settings).toMatchObject({
|
||||
attachDocument: false,
|
||||
documentCompleted: false,
|
||||
ownerDocumentCompleted: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('inherits organisation defaults, allows team overrides, and restores inheritance', () => {
|
||||
const organisationSettings = generateDefaultOrganisationSettings();
|
||||
organisationSettings.emailDocumentSettings = { ...DEFAULT_DOCUMENT_EMAIL_SETTINGS, attachDocument: false };
|
||||
const teamSettings = generateDefaultTeamSettings();
|
||||
|
||||
const inherited = extractDerivedTeamSettings(organisationSettings, teamSettings);
|
||||
expect(extractDerivedDocumentMeta(inherited, undefined).emailSettings.attachDocument).toBe(false);
|
||||
|
||||
teamSettings.emailDocumentSettings = { ...DEFAULT_DOCUMENT_EMAIL_SETTINGS, attachDocument: true };
|
||||
const overridden = extractDerivedTeamSettings(organisationSettings, teamSettings);
|
||||
expect(extractDerivedDocumentMeta(overridden, undefined).emailSettings.attachDocument).toBe(true);
|
||||
|
||||
teamSettings.emailDocumentSettings = null;
|
||||
expect(
|
||||
extractDerivedDocumentMeta(extractDerivedTeamSettings(organisationSettings, teamSettings), undefined)
|
||||
.emailSettings.attachDocument,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('allows a document or template to override inherited attachment defaults in either direction', () => {
|
||||
const settings = generateDefaultOrganisationSettings();
|
||||
|
||||
expect(
|
||||
extractDerivedDocumentMeta(settings, {
|
||||
emailSettings: { ...DEFAULT_DOCUMENT_EMAIL_SETTINGS, attachDocument: false },
|
||||
}).emailSettings.attachDocument,
|
||||
).toBe(false);
|
||||
|
||||
settings.emailDocumentSettings = { ...DEFAULT_DOCUMENT_EMAIL_SETTINGS, attachDocument: false };
|
||||
expect(
|
||||
extractDerivedDocumentMeta(settings, {
|
||||
emailSettings: { ...DEFAULT_DOCUMENT_EMAIL_SETTINGS, attachDocument: true },
|
||||
}).emailSettings.attachDocument,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -14,8 +14,14 @@ export enum DocumentEmailEvents {
|
||||
OwnerDocumentCreated = 'ownerDocumentCreated',
|
||||
}
|
||||
|
||||
export const ZDocumentEmailSettingsSchema = z
|
||||
export const ZDocumentEmailSettingsInputSchema = z
|
||||
.object({
|
||||
attachDocument: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'Whether to attach the completed document PDFs to emails sent to the owner and recipients. Document links are always included.',
|
||||
)
|
||||
.default(true),
|
||||
recipientSigningRequest: z
|
||||
.boolean()
|
||||
.describe('Whether to send an email to all recipients that the document is ready for them to sign.')
|
||||
@@ -55,8 +61,13 @@ export const ZDocumentEmailSettingsSchema = z
|
||||
.describe('Whether to send an email to the document owner when a document is created from a direct template.')
|
||||
.default(true),
|
||||
})
|
||||
.strip()
|
||||
.catch(() => ({ ...DEFAULT_DOCUMENT_EMAIL_SETTINGS }));
|
||||
.strip();
|
||||
|
||||
// Persisted settings may be absent or predate new options. API inputs must use
|
||||
// the input schema so invalid values cannot silently enable document attachments.
|
||||
export const ZDocumentEmailSettingsSchema = ZDocumentEmailSettingsInputSchema.catch(() => ({
|
||||
...DEFAULT_DOCUMENT_EMAIL_SETTINGS,
|
||||
}));
|
||||
|
||||
export type TDocumentEmailSettings = z.infer<typeof ZDocumentEmailSettingsSchema>;
|
||||
|
||||
@@ -68,6 +79,7 @@ export const extractDerivedDocumentEmailSettings = (documentMeta?: DocumentMeta
|
||||
}
|
||||
|
||||
return {
|
||||
attachDocument: emailSettings.attachDocument,
|
||||
recipientSigningRequest: false,
|
||||
recipientRemoved: false,
|
||||
recipientSigned: false,
|
||||
@@ -81,6 +93,7 @@ export const extractDerivedDocumentEmailSettings = (documentMeta?: DocumentMeta
|
||||
};
|
||||
|
||||
export const DEFAULT_DOCUMENT_EMAIL_SETTINGS: TDocumentEmailSettings = {
|
||||
attachDocument: true,
|
||||
recipientSigningRequest: true,
|
||||
recipientRemoved: true,
|
||||
recipientSigned: true,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { msg } from '@lingui/core/macro';
|
||||
import { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZDocumentEmailSettingsSchema } from './document-email';
|
||||
import { ZDocumentEmailSettingsInputSchema } from './document-email';
|
||||
|
||||
/**
|
||||
* The full document response schema.
|
||||
@@ -125,7 +125,7 @@ export const ZDocumentMetaCreateSchema = z.object({
|
||||
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: zEmail().nullish(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.nullish(),
|
||||
emailSettings: ZDocumentEmailSettingsInputSchema.nullish(),
|
||||
envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullish(),
|
||||
reminderSettings: ZEnvelopeReminderSettings.nullish(),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ZDocumentLiteSchema } from '@documenso/lib/types/document';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaDistributionMethodSchema,
|
||||
@@ -39,7 +39,7 @@ export const ZDistributeDocumentRequestSchema = z.object({
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: zEmail().nullish(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaDistributionMethodSchema,
|
||||
@@ -66,7 +66,7 @@ export const ZCreateEmbeddingDocumentRequestSchema = z.object({
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaDistributionMethodSchema,
|
||||
@@ -62,7 +62,7 @@ export const ZCreateEmbeddingTemplateRequestSchema = z.object({
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaDistributionMethodSchema,
|
||||
@@ -67,7 +67,7 @@ export const ZUpdateEmbeddingDocumentRequestSchema = z.object({
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaDistributionMethodSchema,
|
||||
@@ -66,7 +66,7 @@ export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ZEnvelopeExpirationPeriod } from '@documenso/lib/constants/envelope-expiration';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
@@ -89,7 +89,7 @@ export const ZUseEnvelopePayloadSchema = z.object({
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ZEnvelopeReminderSettings } from '@documenso/lib/constants/envelope-rem
|
||||
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
||||
import { ZCssVarsSchema } from '@documenso/lib/types/css-vars';
|
||||
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentMetaDateFormatSchema, ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { ZSanitizeBrandingCssWarningSchema } from '@documenso/lib/utils/sanitize-branding-css';
|
||||
@@ -41,7 +41,7 @@ export const ZUpdateOrganisationSettingsRequestSchema = z.object({
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: zEmail().nullish(),
|
||||
// emailReplyToName: z.string().optional(),
|
||||
emailDocumentSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
emailDocumentSettings: ZDocumentEmailSettingsInputSchema.optional(),
|
||||
|
||||
// AI features settings.
|
||||
aiFeaturesEnabled: z.boolean().optional(),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ZEnvelopeReminderSettings } from '@documenso/lib/constants/envelope-rem
|
||||
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
||||
import { ZCssVarsSchema } from '@documenso/lib/types/css-vars';
|
||||
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentMetaDateFormatSchema, ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { ZSanitizeBrandingCssWarningSchema } from '@documenso/lib/utils/sanitize-branding-css';
|
||||
@@ -44,7 +44,7 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: zEmail().nullish(),
|
||||
// emailReplyToName: z.string().nullish(),
|
||||
emailDocumentSettings: ZDocumentEmailSettingsSchema.nullish(),
|
||||
emailDocumentSettings: ZDocumentEmailSettingsInputSchema.nullish(),
|
||||
|
||||
// Default recipients settings.
|
||||
defaultRecipients: ZDefaultRecipientsSchema.nullish(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ZEnvelopeExpirationPeriod } from '@documenso/lib/constants/envelope-expiration';
|
||||
import { ZDocumentSchema } from '@documenso/lib/types/document';
|
||||
import { ZDocumentAccessAuthTypesSchema, ZDocumentActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
@@ -60,7 +60,7 @@ export const ZTemplateMetaUpsertSchema = z.object({
|
||||
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: zEmail().nullish(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
@@ -142,7 +142,7 @@ export const ZCreateDocumentFromTemplateRequestSchema = z.object({
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsInputSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
|
||||
@@ -21,6 +21,33 @@ export const DocumentEmailCheckboxes = ({ value, onChange, className, hiddenEven
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
<div className="flex flex-row items-center">
|
||||
<Checkbox
|
||||
id="attachDocument"
|
||||
className="h-5 w-5"
|
||||
checked={value.attachDocument ?? true}
|
||||
onCheckedChange={(checked) => onChange({ ...value, attachDocument: Boolean(checked) })}
|
||||
/>
|
||||
|
||||
<label className="ml-2 flex flex-row items-center text-muted-foreground text-sm" htmlFor="attachDocument">
|
||||
<Trans>Attach completed document PDFs to emails</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<p>
|
||||
<Trans>
|
||||
Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable
|
||||
this to send emails with document links only.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
{!isHidden(DocumentEmailEvents.RecipientSigned) && (
|
||||
<div className="flex flex-row items-center">
|
||||
<Checkbox
|
||||
|
||||
Reference in New Issue
Block a user