mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: add document distribution setting (#1437)
Add a document distribution setting which will allow us to further configure how recipients currently receive documents.
This commit is contained in:
@ -12,6 +12,7 @@ import {
|
|||||||
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
SKIP_QUERY_BATCH_META,
|
SKIP_QUERY_BATCH_META,
|
||||||
} from '@documenso/lib/constants/trpc';
|
} from '@documenso/lib/constants/trpc';
|
||||||
|
import { DocumentDistributionMethod, DocumentStatus } from '@documenso/prisma/client';
|
||||||
import type { DocumentWithDetails } from '@documenso/prisma/types/document';
|
import type { DocumentWithDetails } from '@documenso/prisma/types/document';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -177,8 +178,8 @@ export const EditDocumentForm = ({
|
|||||||
stepIndex: 3,
|
stepIndex: 3,
|
||||||
},
|
},
|
||||||
subject: {
|
subject: {
|
||||||
title: msg`Add Subject`,
|
title: msg`Distribute Document`,
|
||||||
description: msg`Add the subject and message you wish to send to signers.`,
|
description: msg`Choose how the document will reach recipients`,
|
||||||
stepIndex: 4,
|
stepIndex: 4,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -307,7 +308,7 @@ export const EditDocumentForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
|
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
|
||||||
const { subject, message } = data.meta;
|
const { subject, message, distributionMethod, emailSettings } = data.meta;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendDocument({
|
await sendDocument({
|
||||||
@ -316,9 +317,12 @@ export const EditDocumentForm = ({
|
|||||||
meta: {
|
meta: {
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
|
distributionMethod,
|
||||||
|
emailSettings,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (distributionMethod === DocumentDistributionMethod.EMAIL) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Document sent`),
|
title: _(msg`Document sent`),
|
||||||
description: _(msg`Your document has been sent successfully.`),
|
description: _(msg`Your document has been sent successfully.`),
|
||||||
@ -326,6 +330,18 @@ export const EditDocumentForm = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.push(documentRootPath);
|
router.push(documentRootPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.status === DocumentStatus.DRAFT) {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Links Generated`),
|
||||||
|
description: _(msg`Signing links have been generated for this document.`),
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
router.push(`${documentRootPath}/${document.id}`);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
|
|||||||
@ -145,6 +145,7 @@ export const TemplatesDataTable = ({
|
|||||||
<UseTemplateDialog
|
<UseTemplateDialog
|
||||||
templateId={row.original.id}
|
templateId={row.original.id}
|
||||||
templateSigningOrder={row.original.templateMeta?.signingOrder}
|
templateSigningOrder={row.original.templateMeta?.signingOrder}
|
||||||
|
documentDistributionMethod={row.original.templateMeta?.distributionMethod}
|
||||||
recipients={row.original.Recipient}
|
recipients={row.original.Recipient}
|
||||||
documentRootPath={documentRootPath}
|
documentRootPath={documentRootPath}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import {
|
|||||||
} from '@documenso/lib/constants/template';
|
} from '@documenso/lib/constants/template';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import { DocumentSigningOrder } from '@documenso/prisma/client';
|
import { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -49,7 +49,7 @@ import { useOptionalCurrentTeam } from '~/providers/team';
|
|||||||
|
|
||||||
const ZAddRecipientsForNewDocumentSchema = z
|
const ZAddRecipientsForNewDocumentSchema = z
|
||||||
.object({
|
.object({
|
||||||
sendDocument: z.boolean(),
|
distributeDocument: z.boolean(),
|
||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
@ -93,12 +93,14 @@ export type UseTemplateDialogProps = {
|
|||||||
templateId: number;
|
templateId: number;
|
||||||
templateSigningOrder?: DocumentSigningOrder | null;
|
templateSigningOrder?: DocumentSigningOrder | null;
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
|
documentDistributionMethod?: DocumentDistributionMethod;
|
||||||
documentRootPath: string;
|
documentRootPath: string;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function UseTemplateDialog({
|
export function UseTemplateDialog({
|
||||||
recipients,
|
recipients,
|
||||||
|
documentDistributionMethod = DocumentDistributionMethod.EMAIL,
|
||||||
documentRootPath,
|
documentRootPath,
|
||||||
templateId,
|
templateId,
|
||||||
templateSigningOrder,
|
templateSigningOrder,
|
||||||
@ -116,7 +118,7 @@ export function UseTemplateDialog({
|
|||||||
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
||||||
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
sendDocument: false,
|
distributeDocument: false,
|
||||||
recipients: recipients
|
recipients: recipients
|
||||||
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
|
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
|
||||||
.map((recipient) => {
|
.map((recipient) => {
|
||||||
@ -147,7 +149,7 @@ export function UseTemplateDialog({
|
|||||||
templateId,
|
templateId,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
recipients: data.recipients,
|
recipients: data.recipients,
|
||||||
sendDocument: data.sendDocument,
|
distributeDocument: data.distributeDocument,
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@ -156,7 +158,16 @@ export function UseTemplateDialog({
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push(`${documentRootPath}/${id}`);
|
let documentPath = `${documentRootPath}/${id}`;
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.distributeDocument &&
|
||||||
|
documentDistributionMethod === DocumentDistributionMethod.NONE
|
||||||
|
) {
|
||||||
|
documentPath += '?action=view-signing-links';
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push(documentPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -295,21 +306,22 @@ export function UseTemplateDialog({
|
|||||||
<div className="mt-4 flex flex-row items-center">
|
<div className="mt-4 flex flex-row items-center">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="sendDocument"
|
name="distributeDocument"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="sendDocument"
|
id="distributeDocument"
|
||||||
className="h-5 w-5"
|
className="h-5 w-5"
|
||||||
checkClassName="dark:text-white text-primary"
|
checkClassName="dark:text-white text-primary"
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
|
||||||
<label
|
<label
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||||
htmlFor="sendDocument"
|
htmlFor="distributeDocument"
|
||||||
>
|
>
|
||||||
<Trans>Send document</Trans>
|
<Trans>Send document</Trans>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@ -320,18 +332,50 @@ export function UseTemplateDialog({
|
|||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
||||||
<p>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
{' '}
|
|
||||||
The document will be immediately sent to recipients if this is
|
The document will be immediately sent to recipients if this is
|
||||||
checked.
|
checked.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<Trans>Otherwise, the document will be created as a draft.</Trans>
|
<Trans>
|
||||||
|
Otherwise, the document will be created as a draft.
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
|
||||||
|
<label
|
||||||
|
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||||
|
htmlFor="distributeDocument"
|
||||||
|
>
|
||||||
|
<Trans>Create as pending</Trans>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger type="button">
|
||||||
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
||||||
|
<p>
|
||||||
|
<Trans>Create the document as pending and ready to sign.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>We won't send anything to notify recipients.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-2">
|
||||||
|
<Trans>
|
||||||
|
We will generate signing links for you, which you can send to
|
||||||
|
the recipients through your method of choice.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@ -347,10 +391,12 @@ export function UseTemplateDialog({
|
|||||||
</DialogClose>
|
</DialogClose>
|
||||||
|
|
||||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||||
{form.getValues('sendDocument') ? (
|
{!form.getValues('distributeDocument') ? (
|
||||||
|
<Trans>Create as draft</Trans>
|
||||||
|
) : documentDistributionMethod === DocumentDistributionMethod.EMAIL ? (
|
||||||
<Trans>Create and send</Trans>
|
<Trans>Create and send</Trans>
|
||||||
) : (
|
) : (
|
||||||
<Trans>Create as draft</Trans>
|
<Trans>Create signing links</Trans>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@ -106,7 +106,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document', async ({ page }) =>
|
|||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
// Add subject and send
|
// Add subject and send
|
||||||
await expect(page.getByRole('heading', { name: 'Add Subject' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Distribute Document' })).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Send' }).click();
|
await page.getByRole('button', { name: 'Send' }).click();
|
||||||
|
|
||||||
await page.waitForURL('/documents');
|
await page.waitForURL('/documents');
|
||||||
@ -190,7 +190,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
|
|||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
// Add subject and send
|
// Add subject and send
|
||||||
await expect(page.getByRole('heading', { name: 'Add Subject' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Distribute Document' })).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Send' }).click();
|
await page.getByRole('button', { name: 'Send' }).click();
|
||||||
|
|
||||||
await page.waitForURL('/documents');
|
await page.waitForURL('/documents');
|
||||||
@ -287,7 +287,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
|
|||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
// Add subject and send
|
// Add subject and send
|
||||||
await expect(page.getByRole('heading', { name: 'Add Subject' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Distribute Document' })).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Send' }).click();
|
await page.getByRole('button', { name: 'Send' }).click();
|
||||||
|
|
||||||
await page.waitForURL('/documents');
|
await page.waitForURL('/documents');
|
||||||
@ -566,7 +566,7 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Add Subject' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Distribute Document' })).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Send' }).click();
|
await page.getByRole('button', { name: 'Send' }).click();
|
||||||
|
|
||||||
await page.waitForURL('/documents');
|
await page.waitForURL('/documents');
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { MessageDescriptor } from '@lingui/core';
|
import type { MessageDescriptor } from '@lingui/core';
|
||||||
import { msg } from '@lingui/macro';
|
import { msg } from '@lingui/macro';
|
||||||
|
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import { DocumentDistributionMethod, DocumentStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const DOCUMENT_STATUS: {
|
export const DOCUMENT_STATUS: {
|
||||||
[status in DocumentStatus]: { description: MessageDescriptor };
|
[status in DocumentStatus]: { description: MessageDescriptor };
|
||||||
@ -16,3 +16,19 @@ export const DOCUMENT_STATUS: {
|
|||||||
description: msg`Pending`,
|
description: msg`Pending`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DocumentDistributionMethodTypeData = {
|
||||||
|
value: DocumentDistributionMethod;
|
||||||
|
description: MessageDescriptor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DOCUMENT_DISTRIBUTION_METHODS: Record<string, DocumentDistributionMethodTypeData> = {
|
||||||
|
[DocumentDistributionMethod.EMAIL]: {
|
||||||
|
value: DocumentDistributionMethod.EMAIL,
|
||||||
|
description: msg`Email`,
|
||||||
|
},
|
||||||
|
[DocumentDistributionMethod.NONE]: {
|
||||||
|
value: DocumentDistributionMethod.NONE,
|
||||||
|
description: msg`None`,
|
||||||
|
},
|
||||||
|
} satisfies Record<DocumentDistributionMethod, DocumentDistributionMethodTypeData>;
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import {
|
|||||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||||
} from '../../../constants/recipient-roles';
|
} from '../../../constants/recipient-roles';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
||||||
|
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||||
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
|
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
|
||||||
import { renderCustomEmailTemplate } from '../../../utils/render-custom-email-template';
|
import { renderCustomEmailTemplate } from '../../../utils/render-custom-email-template';
|
||||||
@ -81,6 +82,14 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||||
|
document.documentMeta,
|
||||||
|
).recipientSigningRequest;
|
||||||
|
|
||||||
|
if (!isRecipientSigningRequestEmailEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const customEmail = document?.documentMeta;
|
const customEmail = document?.documentMeta;
|
||||||
const isDirectTemplate = document.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
const isDirectTemplate = document.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
||||||
const isTeamDocument = document.teamId !== null;
|
const isTeamDocument = document.teamId !== null;
|
||||||
|
|||||||
@ -7,9 +7,10 @@ import {
|
|||||||
diffDocumentMetaChanges,
|
diffDocumentMetaChanges,
|
||||||
} from '@documenso/lib/utils/document-audit-logs';
|
} from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import type { DocumentSigningOrder } from '@documenso/prisma/client';
|
import type { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||||
|
import type { TDocumentEmailSettings } from '../../types/document-email';
|
||||||
|
|
||||||
export type CreateDocumentMetaOptions = {
|
export type CreateDocumentMetaOptions = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
@ -19,7 +20,9 @@ export type CreateDocumentMetaOptions = {
|
|||||||
password?: string;
|
password?: string;
|
||||||
dateFormat?: string;
|
dateFormat?: string;
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
|
emailSettings?: TDocumentEmailSettings;
|
||||||
signingOrder?: DocumentSigningOrder;
|
signingOrder?: DocumentSigningOrder;
|
||||||
|
distributionMethod?: DocumentDistributionMethod;
|
||||||
typedSignatureEnabled?: boolean;
|
typedSignatureEnabled?: boolean;
|
||||||
language?: SupportedLanguageCodes;
|
language?: SupportedLanguageCodes;
|
||||||
userId: number;
|
userId: number;
|
||||||
@ -36,6 +39,8 @@ export const upsertDocumentMeta = async ({
|
|||||||
userId,
|
userId,
|
||||||
redirectUrl,
|
redirectUrl,
|
||||||
signingOrder,
|
signingOrder,
|
||||||
|
emailSettings,
|
||||||
|
distributionMethod,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
language,
|
language,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
@ -88,6 +93,8 @@ export const upsertDocumentMeta = async ({
|
|||||||
documentId,
|
documentId,
|
||||||
redirectUrl,
|
redirectUrl,
|
||||||
signingOrder,
|
signingOrder,
|
||||||
|
emailSettings,
|
||||||
|
distributionMethod,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
language,
|
language,
|
||||||
},
|
},
|
||||||
@ -99,6 +106,8 @@ export const upsertDocumentMeta = async ({
|
|||||||
timezone,
|
timezone,
|
||||||
redirectUrl,
|
redirectUrl,
|
||||||
signingOrder,
|
signingOrder,
|
||||||
|
emailSettings,
|
||||||
|
distributionMethod,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
language,
|
language,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
|||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||||
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
@ -178,6 +179,14 @@ const handleDocumentOwnerDelete = async ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isDocumentDeleteEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||||
|
document.documentMeta,
|
||||||
|
).documentDeleted;
|
||||||
|
|
||||||
|
if (!isDocumentDeleteEmailEnabled) {
|
||||||
|
return deletedDocument;
|
||||||
|
}
|
||||||
|
|
||||||
// Send cancellation emails to recipients.
|
// Send cancellation emails to recipients.
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
document.Recipient.map(async (recipient) => {
|
document.Recipient.map(async (recipient) => {
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import type { Prisma } from '@documenso/prisma/client';
|
|||||||
|
|
||||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getDocumentWhereInput } from './get-document-by-id';
|
import { getDocumentWhereInput } from './get-document-by-id';
|
||||||
|
|
||||||
@ -89,6 +90,14 @@ export const resendDocument = async ({
|
|||||||
throw new Error('Can not send completed document');
|
throw new Error('Can not send completed document');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||||
|
document.documentMeta,
|
||||||
|
).recipientSigningRequest;
|
||||||
|
|
||||||
|
if (!isRecipientSigningRequestEmailEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
document.Recipient.map(async (recipient) => {
|
document.Recipient.map(async (recipient) => {
|
||||||
if (recipient.role === RecipientRole.CC) {
|
if (recipient.role === RecipientRole.CC) {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { DocumentSource } from '@documenso/prisma/client';
|
|||||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||||
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { getFile } from '../../universal/upload/get-file';
|
import { getFile } from '../../universal/upload/get-file';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
@ -66,8 +67,15 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
|||||||
|
|
||||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||||
|
|
||||||
// If the document owner is not a recipient then send the email to them separately
|
const isDocumentCompletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||||
if (!document.Recipient.find((recipient) => recipient.email === owner.email)) {
|
document.documentMeta,
|
||||||
|
).documentCompleted;
|
||||||
|
|
||||||
|
// If the document owner is not a recipient, OR recipient emails are disabled, then send the email to them separately.
|
||||||
|
if (
|
||||||
|
!document.Recipient.find((recipient) => recipient.email === owner.email) ||
|
||||||
|
!isDocumentCompletedEmailEnabled
|
||||||
|
) {
|
||||||
const template = createElement(DocumentCompletedEmailTemplate, {
|
const template = createElement(DocumentCompletedEmailTemplate, {
|
||||||
documentName: document.title,
|
documentName: document.title,
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
@ -119,6 +127,10 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isDocumentCompletedEmailEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
document.Recipient.map(async (recipient) => {
|
document.Recipient.map(async (recipient) => {
|
||||||
const customEmailTemplate = {
|
const customEmailTemplate = {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { prisma } from '@documenso/prisma';
|
|||||||
|
|
||||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
|
|
||||||
export interface SendDeleteEmailOptions {
|
export interface SendDeleteEmailOptions {
|
||||||
@ -22,6 +23,7 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
|||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
User: true,
|
User: true,
|
||||||
|
documentMeta: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -29,6 +31,14 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
|||||||
throw new Error('Document not found');
|
throw new Error('Document not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||||
|
document.documentMeta,
|
||||||
|
).documentDeleted;
|
||||||
|
|
||||||
|
if (!isDocumentDeletedEmailEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { email, name } = document.User;
|
const { email, name } = document.User;
|
||||||
|
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { jobs } from '../../jobs/client';
|
import { jobs } from '../../jobs/client';
|
||||||
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { getFile } from '../../universal/upload/get-file';
|
import { getFile } from '../../universal/upload/get-file';
|
||||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||||
@ -29,7 +30,7 @@ export const sendDocument = async ({
|
|||||||
documentId,
|
documentId,
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
sendEmail = true,
|
sendEmail,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: SendDocumentOptions) => {
|
}: SendDocumentOptions) => {
|
||||||
const user = await prisma.user.findFirstOrThrow({
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
@ -156,7 +157,14 @@ export const sendDocument = async ({
|
|||||||
// throw new Error('Some signers have not been assigned a signature field.');
|
// throw new Error('Some signers have not been assigned a signature field.');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
if (sendEmail) {
|
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||||
|
document.documentMeta,
|
||||||
|
).recipientSigningRequest;
|
||||||
|
|
||||||
|
// Only send email if one of the following is true:
|
||||||
|
// - It is explicitly set
|
||||||
|
// - The email is enabled for signing requests AND sendEmail is undefined
|
||||||
|
if (sendEmail || (isRecipientSigningRequestEmailEnabled && sendEmail === undefined)) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
recipientsToNotify.map(async (recipient) => {
|
recipientsToNotify.map(async (recipient) => {
|
||||||
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
|
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { prisma } from '@documenso/prisma';
|
|||||||
|
|
||||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
|
|
||||||
export interface SendPendingEmailOptions {
|
export interface SendPendingEmailOptions {
|
||||||
@ -43,6 +44,14 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
|||||||
throw new Error('Document has no recipients');
|
throw new Error('Document has no recipients');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isDocumentPendingEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||||
|
document.documentMeta,
|
||||||
|
).documentPending;
|
||||||
|
|
||||||
|
if (!isDocumentPendingEmailEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [recipient] = document.Recipient;
|
const [recipient] = document.Recipient;
|
||||||
|
|
||||||
const { email, name } = recipient;
|
const { email, name } = recipient;
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
|||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||||
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
@ -40,8 +41,16 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
|
|||||||
|
|
||||||
const { status, User: user } = document;
|
const { status, User: user } = document;
|
||||||
|
|
||||||
|
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||||
|
document.documentMeta,
|
||||||
|
).documentDeleted;
|
||||||
|
|
||||||
// if the document is pending, send cancellation emails to all recipients
|
// if the document is pending, send cancellation emails to all recipients
|
||||||
if (status === DocumentStatus.PENDING && document.Recipient.length > 0) {
|
if (
|
||||||
|
status === DocumentStatus.PENDING &&
|
||||||
|
document.Recipient.length > 0 &&
|
||||||
|
isDocumentDeletedEmailEnabled
|
||||||
|
) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
document.Recipient.map(async (recipient) => {
|
document.Recipient.map(async (recipient) => {
|
||||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
|||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { canRecipientBeModified } from '../../utils/recipients';
|
import { canRecipientBeModified } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
|
|
||||||
@ -280,10 +281,14 @@ export const setRecipientsForDocument = async ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||||
|
document.documentMeta,
|
||||||
|
).recipientRemoved;
|
||||||
|
|
||||||
// Send emails to deleted recipients.
|
// Send emails to deleted recipients.
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
removedRecipients.map(async (recipient) => {
|
removedRecipients.map(async (recipient) => {
|
||||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
if (recipient.sendStatus !== SendStatus.SENT || !isRecipientRemovedEmailEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -275,6 +275,7 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
subject: metaEmailSubject,
|
subject: metaEmailSubject,
|
||||||
language: metaLanguage,
|
language: metaLanguage,
|
||||||
signingOrder: metaSigningOrder,
|
signingOrder: metaSigningOrder,
|
||||||
|
distributionMethod: template.templateMeta?.distributionMethod,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import type { DocumentDistributionMethod } from '@documenso/prisma/client';
|
||||||
import {
|
import {
|
||||||
DocumentSigningOrder,
|
DocumentSigningOrder,
|
||||||
DocumentSource,
|
DocumentSource,
|
||||||
@ -62,6 +63,7 @@ export type CreateDocumentFromTemplateOptions = {
|
|||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
signingOrder?: DocumentSigningOrder;
|
signingOrder?: DocumentSigningOrder;
|
||||||
language?: SupportedLanguageCodes;
|
language?: SupportedLanguageCodes;
|
||||||
|
distributionMethod?: DocumentDistributionMethod;
|
||||||
};
|
};
|
||||||
requestMetadata?: RequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
@ -177,6 +179,9 @@ export const createDocumentFromTemplate = async ({
|
|||||||
password: override?.password || template.templateMeta?.password,
|
password: override?.password || template.templateMeta?.password,
|
||||||
dateFormat: override?.dateFormat || template.templateMeta?.dateFormat,
|
dateFormat: override?.dateFormat || template.templateMeta?.dateFormat,
|
||||||
redirectUrl: override?.redirectUrl || template.templateMeta?.redirectUrl,
|
redirectUrl: override?.redirectUrl || template.templateMeta?.redirectUrl,
|
||||||
|
distributionMethod:
|
||||||
|
override?.distributionMethod || template.templateMeta?.distributionMethod,
|
||||||
|
emailSettings: template.templateMeta?.emailSettings || undefined,
|
||||||
signingOrder:
|
signingOrder:
|
||||||
override?.signingOrder ||
|
override?.signingOrder ||
|
||||||
template.templateMeta?.signingOrder ||
|
template.templateMeta?.signingOrder ||
|
||||||
|
|||||||
@ -60,7 +60,10 @@ export const duplicateTemplate = async ({
|
|||||||
|
|
||||||
if (template.templateMeta) {
|
if (template.templateMeta) {
|
||||||
templateMeta = {
|
templateMeta = {
|
||||||
create: omit(template.templateMeta, ['id', 'templateId']),
|
create: {
|
||||||
|
...omit(template.templateMeta, ['id', 'templateId']),
|
||||||
|
emailSettings: template.templateMeta.emailSettings || undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,7 @@ export const findTemplates = async ({
|
|||||||
templateMeta: {
|
templateMeta: {
|
||||||
select: {
|
select: {
|
||||||
signingOrder: true,
|
signingOrder: true,
|
||||||
|
distributionMethod: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
directLink: {
|
directLink: {
|
||||||
|
|||||||
@ -112,9 +112,11 @@ export const updateTemplateSettings = async ({
|
|||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
...meta,
|
...meta,
|
||||||
|
emailSettings: meta?.emailSettings || undefined,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
...meta,
|
...meta,
|
||||||
|
emailSettings: meta?.emailSettings || undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
52
packages/lib/types/document-email.ts
Normal file
52
packages/lib/types/document-email.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { DocumentMeta } from '@documenso/prisma/client';
|
||||||
|
import { DocumentDistributionMethod } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
export enum DocumentEmailEvents {
|
||||||
|
RecipientSigningRequest = 'recipientSigningRequest',
|
||||||
|
RecipientRemoved = 'recipientRemoved',
|
||||||
|
DocumentPending = 'documentPending',
|
||||||
|
DocumentCompleted = 'documentCompleted',
|
||||||
|
DocumentDeleted = 'documentDeleted',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ZDocumentEmailSettingsSchema = z
|
||||||
|
.object({
|
||||||
|
recipientSigningRequest: z.boolean().default(true),
|
||||||
|
recipientRemoved: z.boolean().default(true),
|
||||||
|
documentPending: z.boolean().default(true),
|
||||||
|
documentCompleted: z.boolean().default(true),
|
||||||
|
documentDeleted: z.boolean().default(true),
|
||||||
|
})
|
||||||
|
.strip()
|
||||||
|
.catch(() => ({
|
||||||
|
recipientSigningRequest: true,
|
||||||
|
recipientRemoved: true,
|
||||||
|
documentPending: true,
|
||||||
|
documentCompleted: true,
|
||||||
|
documentDeleted: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export type TDocumentEmailSettings = z.infer<typeof ZDocumentEmailSettingsSchema>;
|
||||||
|
|
||||||
|
export const extractDerivedDocumentEmailSettings = (
|
||||||
|
documentMeta?: DocumentMeta | null,
|
||||||
|
): TDocumentEmailSettings => {
|
||||||
|
const emailSettings = ZDocumentEmailSettingsSchema.parse(documentMeta?.emailSettings ?? {});
|
||||||
|
|
||||||
|
if (
|
||||||
|
!documentMeta?.distributionMethod ||
|
||||||
|
documentMeta?.distributionMethod === DocumentDistributionMethod.EMAIL
|
||||||
|
) {
|
||||||
|
return emailSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
recipientSigningRequest: false,
|
||||||
|
recipientRemoved: false,
|
||||||
|
documentPending: false,
|
||||||
|
documentCompleted: false,
|
||||||
|
documentDeleted: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "DocumentDistributionMethod" AS ENUM ('EMAIL', 'NONE');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "DocumentMeta" ADD COLUMN "distributionMethod" "DocumentDistributionMethod" NOT NULL DEFAULT 'EMAIL',
|
||||||
|
ADD COLUMN "emailSettings" JSONB;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "TemplateMeta" ADD COLUMN "distributionMethod" "DocumentDistributionMethod" NOT NULL DEFAULT 'EMAIL',
|
||||||
|
ADD COLUMN "emailSettings" JSONB;
|
||||||
@ -358,6 +358,11 @@ model DocumentData {
|
|||||||
Template Template?
|
Template Template?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum DocumentDistributionMethod {
|
||||||
|
EMAIL
|
||||||
|
NONE
|
||||||
|
}
|
||||||
|
|
||||||
model DocumentMeta {
|
model DocumentMeta {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
subject String?
|
subject String?
|
||||||
@ -371,6 +376,8 @@ model DocumentMeta {
|
|||||||
signingOrder DocumentSigningOrder @default(PARALLEL)
|
signingOrder DocumentSigningOrder @default(PARALLEL)
|
||||||
typedSignatureEnabled Boolean @default(false)
|
typedSignatureEnabled Boolean @default(false)
|
||||||
language String @default("en")
|
language String @default("en")
|
||||||
|
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
||||||
|
emailSettings Json?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ReadStatus {
|
enum ReadStatus {
|
||||||
@ -614,6 +621,8 @@ model TemplateMeta {
|
|||||||
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||||
redirectUrl String?
|
redirectUrl String?
|
||||||
language String @default("en")
|
language String @default("en")
|
||||||
|
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
||||||
|
emailSettings Json?
|
||||||
}
|
}
|
||||||
|
|
||||||
model Template {
|
model Template {
|
||||||
|
|||||||
@ -413,7 +413,15 @@ export const documentRouter = router({
|
|||||||
try {
|
try {
|
||||||
const { documentId, teamId, meta } = input;
|
const { documentId, teamId, meta } = input;
|
||||||
|
|
||||||
if (meta.message || meta.subject || meta.timezone || meta.dateFormat || meta.redirectUrl) {
|
if (
|
||||||
|
meta.message ||
|
||||||
|
meta.subject ||
|
||||||
|
meta.timezone ||
|
||||||
|
meta.dateFormat ||
|
||||||
|
meta.redirectUrl ||
|
||||||
|
meta.distributionMethod ||
|
||||||
|
meta.emailSettings
|
||||||
|
) {
|
||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
documentId,
|
documentId,
|
||||||
subject: meta.subject,
|
subject: meta.subject,
|
||||||
@ -421,7 +429,9 @@ export const documentRouter = router({
|
|||||||
dateFormat: meta.dateFormat,
|
dateFormat: meta.dateFormat,
|
||||||
timezone: meta.timezone,
|
timezone: meta.timezone,
|
||||||
redirectUrl: meta.redirectUrl,
|
redirectUrl: meta.redirectUrl,
|
||||||
|
distributionMethod: meta.distributionMethod,
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
|
emailSettings: meta.emailSettings,
|
||||||
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,9 +5,11 @@ import {
|
|||||||
ZDocumentAccessAuthTypesSchema,
|
ZDocumentAccessAuthTypesSchema,
|
||||||
ZDocumentActionAuthTypesSchema,
|
ZDocumentActionAuthTypesSchema,
|
||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||||
import {
|
import {
|
||||||
|
DocumentDistributionMethod,
|
||||||
DocumentSigningOrder,
|
DocumentSigningOrder,
|
||||||
DocumentSource,
|
DocumentSource,
|
||||||
DocumentStatus,
|
DocumentStatus,
|
||||||
@ -155,6 +157,7 @@ export const ZSendDocumentMutationSchema = z.object({
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
timezone: z.string().optional(),
|
timezone: z.string().optional(),
|
||||||
dateFormat: z.string().optional(),
|
dateFormat: z.string().optional(),
|
||||||
|
distributionMethod: z.nativeEnum(DocumentDistributionMethod).optional(),
|
||||||
redirectUrl: z
|
redirectUrl: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@ -162,6 +165,7 @@ export const ZSendDocumentMutationSchema = z.object({
|
|||||||
message:
|
message:
|
||||||
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
|
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
|
||||||
}),
|
}),
|
||||||
|
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -120,7 +120,7 @@ export const templateRouter = router({
|
|||||||
requestMetadata,
|
requestMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (input.sendDocument) {
|
if (input.distributeDocument) {
|
||||||
document = await sendDocument({
|
document = await sendDocument({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
|
|||||||
@ -5,9 +5,14 @@ import {
|
|||||||
ZDocumentAccessAuthTypesSchema,
|
ZDocumentAccessAuthTypesSchema,
|
||||||
ZDocumentActionAuthTypesSchema,
|
ZDocumentActionAuthTypesSchema,
|
||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||||
import { DocumentSigningOrder, TemplateType } from '@documenso/prisma/client';
|
import {
|
||||||
|
DocumentDistributionMethod,
|
||||||
|
DocumentSigningOrder,
|
||||||
|
TemplateType,
|
||||||
|
} from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
|
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
|
||||||
|
|
||||||
@ -41,7 +46,7 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
|||||||
const emails = recipients.map((signer) => signer.email);
|
const emails = recipients.map((signer) => signer.email);
|
||||||
return new Set(emails).size === emails.length;
|
return new Set(emails).size === emails.length;
|
||||||
}, 'Recipients must have unique emails'),
|
}, 'Recipients must have unique emails'),
|
||||||
sendDocument: z.boolean().optional(),
|
distributeDocument: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZDuplicateTemplateMutationSchema = z.object({
|
export const ZDuplicateTemplateMutationSchema = z.object({
|
||||||
@ -99,6 +104,8 @@ export const ZUpdateTemplateSettingsMutationSchema = z.object({
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
timezone: z.string(),
|
timezone: z.string(),
|
||||||
dateFormat: z.string(),
|
dateFormat: z.string(),
|
||||||
|
distributionMethod: z.nativeEnum(DocumentDistributionMethod),
|
||||||
|
emailSettings: ZDocumentEmailSettingsSchema,
|
||||||
redirectUrl: z
|
redirectUrl: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
|
|||||||
222
packages/ui/components/document/document-email-checkboxes.tsx
Normal file
222
packages/ui/components/document/document-email-checkboxes.tsx
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import { Trans } from '@lingui/macro';
|
||||||
|
import { InfoIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
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 DocumentEmailCheckboxesProps = {
|
||||||
|
value: Value;
|
||||||
|
onChange: (value: Value) => void;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocumentEmailCheckboxes = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
}: DocumentEmailCheckboxesProps) => {
|
||||||
|
return (
|
||||||
|
<div className={cn('space-y-3', className)}>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Checkbox
|
||||||
|
id={DocumentEmailEvents.RecipientSigningRequest}
|
||||||
|
className="h-5 w-5"
|
||||||
|
checkClassName="dark:text-white text-primary"
|
||||||
|
checked={value.recipientSigningRequest}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onChange({ ...value, [DocumentEmailEvents.RecipientSigningRequest]: Boolean(checked) })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label
|
||||||
|
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||||
|
htmlFor={DocumentEmailEvents.RecipientSigningRequest}
|
||||||
|
>
|
||||||
|
<Trans>Send recipient signing request email</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>Recipient signing request email</Trans>
|
||||||
|
</strong>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
This email is sent to the recipient requesting them to sign the document.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Checkbox
|
||||||
|
id={DocumentEmailEvents.RecipientRemoved}
|
||||||
|
className="h-5 w-5"
|
||||||
|
checkClassName="dark:text-white text-primary"
|
||||||
|
checked={value.recipientRemoved}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onChange({ ...value, [DocumentEmailEvents.RecipientRemoved]: Boolean(checked) })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label
|
||||||
|
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||||
|
htmlFor={DocumentEmailEvents.RecipientRemoved}
|
||||||
|
>
|
||||||
|
<Trans>Send recipient removed email</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>Recipient removed email</Trans>
|
||||||
|
</strong>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
This email is sent to the recipient if they are removed from a pending document.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Checkbox
|
||||||
|
id={DocumentEmailEvents.DocumentPending}
|
||||||
|
className="h-5 w-5"
|
||||||
|
checkClassName="dark:text-white text-primary"
|
||||||
|
checked={value.documentPending}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onChange({ ...value, [DocumentEmailEvents.DocumentPending]: Boolean(checked) })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label
|
||||||
|
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||||
|
htmlFor={DocumentEmailEvents.DocumentPending}
|
||||||
|
>
|
||||||
|
<Trans>Send document pending email</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 pending email</Trans>
|
||||||
|
</strong>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
This email will be sent to the recipient who has just signed the document, if
|
||||||
|
there are still other recipients who have not signed yet.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Checkbox
|
||||||
|
id={DocumentEmailEvents.DocumentCompleted}
|
||||||
|
className="h-5 w-5"
|
||||||
|
checkClassName="dark:text-white text-primary"
|
||||||
|
checked={value.documentCompleted}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onChange({ ...value, [DocumentEmailEvents.DocumentCompleted]: Boolean(checked) })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label
|
||||||
|
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||||
|
htmlFor={DocumentEmailEvents.DocumentCompleted}
|
||||||
|
>
|
||||||
|
<Trans>Send document completed email</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</Trans>
|
||||||
|
</strong>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
This will be sent to all recipients once the document has been fully completed.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Checkbox
|
||||||
|
id={DocumentEmailEvents.DocumentDeleted}
|
||||||
|
className="h-5 w-5"
|
||||||
|
checkClassName="dark:text-white text-primary"
|
||||||
|
checked={value.documentDeleted}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onChange({ ...value, [DocumentEmailEvents.DocumentDeleted]: Boolean(checked) })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label
|
||||||
|
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||||
|
htmlFor={DocumentEmailEvents.DocumentDeleted}
|
||||||
|
>
|
||||||
|
<Trans>Send document deleted email</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 deleted email</Trans>
|
||||||
|
</strong>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
This will be sent to all recipients if a pending document has been deleted.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -2,18 +2,32 @@
|
|||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
|
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import {
|
||||||
|
DocumentDistributionMethod,
|
||||||
|
DocumentStatus,
|
||||||
|
RecipientRole,
|
||||||
|
} from '@documenso/prisma/client';
|
||||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||||
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
|
|
||||||
|
import { CopyTextButton } from '../../components/common/copy-text-button';
|
||||||
|
import { DocumentEmailCheckboxes } from '../../components/document/document-email-checkboxes';
|
||||||
|
import { AvatarWithText } from '../avatar';
|
||||||
import { FormErrorMessage } from '../form/form-error-message';
|
import { FormErrorMessage } from '../form/form-error-message';
|
||||||
import { Input } from '../input';
|
import { Input } from '../input';
|
||||||
import { Label } from '../label';
|
import { Label } from '../label';
|
||||||
import { useStep } from '../stepper';
|
import { useStep } from '../stepper';
|
||||||
import { Textarea } from '../textarea';
|
import { Textarea } from '../textarea';
|
||||||
|
import { toast } from '../use-toast';
|
||||||
import { type TAddSubjectFormSchema, ZAddSubjectFormSchema } from './add-subject.types';
|
import { type TAddSubjectFormSchema, ZAddSubjectFormSchema } from './add-subject.types';
|
||||||
import {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
@ -42,20 +56,45 @@ export const AddSubjectFormPartial = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
isDocumentPdfLoaded,
|
isDocumentPdfLoaded,
|
||||||
}: AddSubjectFormProps) => {
|
}: AddSubjectFormProps) => {
|
||||||
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
watch,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<TAddSubjectFormSchema>({
|
} = useForm<TAddSubjectFormSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
meta: {
|
meta: {
|
||||||
subject: document.documentMeta?.subject ?? '',
|
subject: document.documentMeta?.subject ?? '',
|
||||||
message: document.documentMeta?.message ?? '',
|
message: document.documentMeta?.message ?? '',
|
||||||
|
distributionMethod:
|
||||||
|
document.documentMeta?.distributionMethod || DocumentDistributionMethod.EMAIL,
|
||||||
|
emailSettings: ZDocumentEmailSettingsSchema.parse(document?.documentMeta?.emailSettings),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
resolver: zodResolver(ZAddSubjectFormSchema),
|
resolver: zodResolver(ZAddSubjectFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const GoNextLabel = {
|
||||||
|
[DocumentDistributionMethod.EMAIL]: {
|
||||||
|
[DocumentStatus.DRAFT]: msg`Send`,
|
||||||
|
[DocumentStatus.PENDING]: recipients.some((recipient) => recipient.sendStatus === 'SENT')
|
||||||
|
? msg`Resend`
|
||||||
|
: msg`Send`,
|
||||||
|
[DocumentStatus.COMPLETED]: msg`Update`,
|
||||||
|
},
|
||||||
|
[DocumentDistributionMethod.NONE]: {
|
||||||
|
[DocumentStatus.DRAFT]: msg`Generate Links`,
|
||||||
|
[DocumentStatus.PENDING]: msg`View Document`,
|
||||||
|
[DocumentStatus.COMPLETED]: msg`View Document`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const distributionMethod = watch('meta.distributionMethod');
|
||||||
|
const emailSettings = watch('meta.emailSettings');
|
||||||
|
|
||||||
const onFormSubmit = handleSubmit(onSubmit);
|
const onFormSubmit = handleSubmit(onSubmit);
|
||||||
const { currentStep, totalSteps, previousStep } = useStep();
|
const { currentStep, totalSteps, previousStep } = useStep();
|
||||||
|
|
||||||
@ -72,7 +111,33 @@ export const AddSubjectFormPartial = ({
|
|||||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="flex flex-col gap-y-4">
|
<Tabs
|
||||||
|
onValueChange={(value) =>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
setValue('meta.distributionMethod', value as DocumentDistributionMethod)
|
||||||
|
}
|
||||||
|
value={distributionMethod}
|
||||||
|
className="mb-2"
|
||||||
|
>
|
||||||
|
<TabsList className="w-full">
|
||||||
|
<TabsTrigger className="w-full" value={DocumentDistributionMethod.EMAIL}>
|
||||||
|
Email
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger className="w-full" value={DocumentDistributionMethod.NONE}>
|
||||||
|
None
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{distributionMethod === DocumentDistributionMethod.EMAIL && (
|
||||||
|
<motion.div
|
||||||
|
key={'Emails'}
|
||||||
|
initial={{ opacity: 0, y: 15 }}
|
||||||
|
animate={{ opacity: 1, y: 0, transition: { duration: 0.3 } }}
|
||||||
|
exit={{ opacity: 0, transition: { duration: 0.15 } }}
|
||||||
|
className="flex flex-col gap-y-4 rounded-lg border p-4"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="subject">
|
<Label htmlFor="subject">
|
||||||
<Trans>
|
<Trans>
|
||||||
@ -106,12 +171,98 @@ export const AddSubjectFormPartial = ({
|
|||||||
|
|
||||||
<FormErrorMessage
|
<FormErrorMessage
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
error={typeof errors.meta?.message !== 'string' ? errors.meta?.message : undefined}
|
error={
|
||||||
|
typeof errors.meta?.message !== 'string' ? errors.meta?.message : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DocumentSendEmailMessageHelper />
|
<DocumentSendEmailMessageHelper />
|
||||||
|
|
||||||
|
<DocumentEmailCheckboxes
|
||||||
|
className="mt-2"
|
||||||
|
value={emailSettings}
|
||||||
|
onChange={(value) => setValue('meta.emailSettings', value)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{distributionMethod === DocumentDistributionMethod.NONE && (
|
||||||
|
<motion.div
|
||||||
|
key={'Links'}
|
||||||
|
initial={{ opacity: 0, y: 15 }}
|
||||||
|
animate={{ opacity: 1, y: 0, transition: { duration: 0.3 } }}
|
||||||
|
exit={{ opacity: 0, transition: { duration: 0.15 } }}
|
||||||
|
className="rounded-lg border"
|
||||||
|
>
|
||||||
|
{document.status === DocumentStatus.DRAFT ? (
|
||||||
|
<div className="text-muted-foreground py-16 text-center text-sm">
|
||||||
|
<p>
|
||||||
|
<Trans>We won't send anything to notify recipients.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-2">
|
||||||
|
<Trans>
|
||||||
|
We will generate signing links for with you, which you can send to the
|
||||||
|
recipients through your method of choice.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<ul className="text-muted-foreground divide-y">
|
||||||
|
{recipients.length === 0 && (
|
||||||
|
<li className="flex flex-col items-center justify-center py-6 text-sm">
|
||||||
|
<Trans>No recipients</Trans>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{recipients.map((recipient) => (
|
||||||
|
<li
|
||||||
|
key={recipient.id}
|
||||||
|
className="flex items-center justify-between px-4 py-3 text-sm"
|
||||||
|
>
|
||||||
|
<AvatarWithText
|
||||||
|
avatarFallback={recipient.email.slice(0, 1).toUpperCase()}
|
||||||
|
primaryText={
|
||||||
|
<p className="text-muted-foreground text-sm">{recipient.email}</p>
|
||||||
|
}
|
||||||
|
secondaryText={
|
||||||
|
<p className="text-muted-foreground/70 text-xs">
|
||||||
|
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{recipient.role !== RecipientRole.CC && (
|
||||||
|
<CopyTextButton
|
||||||
|
value={formatSigningLink(recipient.token)}
|
||||||
|
onCopySuccess={() => {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Copied to clipboard`),
|
||||||
|
description: _(
|
||||||
|
msg`The signing link has been copied to your clipboard.`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
badgeContentUncopied={
|
||||||
|
<p className="ml-1 text-xs">
|
||||||
|
<Trans>Copy</Trans>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
badgeContentCopied={
|
||||||
|
<p className="ml-1 text-xs">
|
||||||
|
<Trans>Copied</Trans>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFlowFormContainerContent>
|
</DocumentFlowFormContainerContent>
|
||||||
|
|
||||||
@ -121,7 +272,7 @@ export const AddSubjectFormPartial = ({
|
|||||||
<DocumentFlowFormContainerActions
|
<DocumentFlowFormContainerActions
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
goNextLabel={document.status === DocumentStatus.DRAFT ? msg`Send` : msg`Update`}
|
goNextLabel={GoNextLabel[distributionMethod][document.status]}
|
||||||
onGoBackClick={previousStep}
|
onGoBackClick={previousStep}
|
||||||
onGoNextClick={() => void onFormSubmit()}
|
onGoNextClick={() => void onFormSubmit()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,9 +1,18 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
|
|
||||||
|
import { DocumentDistributionMethod } from '.prisma/client';
|
||||||
|
|
||||||
export const ZAddSubjectFormSchema = z.object({
|
export const ZAddSubjectFormSchema = z.object({
|
||||||
meta: z.object({
|
meta: z.object({
|
||||||
subject: z.string(),
|
subject: z.string(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
distributionMethod: z
|
||||||
|
.nativeEnum(DocumentDistributionMethod)
|
||||||
|
.optional()
|
||||||
|
.default(DocumentDistributionMethod.EMAIL),
|
||||||
|
emailSettings: ZDocumentEmailSettingsSchema,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,17 @@ import { useEffect } from 'react';
|
|||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { InfoIcon } from 'lucide-react';
|
import { InfoIcon } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||||
|
import { DOCUMENT_DISTRIBUTION_METHODS } from '@documenso/lib/constants/document';
|
||||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||||
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
import { type Field, type Recipient } from '@documenso/prisma/client';
|
import { DocumentDistributionMethod, type Field, type Recipient } from '@documenso/prisma/client';
|
||||||
import type { TemplateWithData } from '@documenso/prisma/types/template';
|
import type { TemplateWithData } from '@documenso/prisma/types/template';
|
||||||
import {
|
import {
|
||||||
DocumentGlobalAuthAccessSelect,
|
DocumentGlobalAuthAccessSelect,
|
||||||
@ -37,6 +40,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
|
|
||||||
|
import { DocumentEmailCheckboxes } from '../../components/document/document-email-checkboxes';
|
||||||
import { Combobox } from '../combobox';
|
import { Combobox } from '../combobox';
|
||||||
import {
|
import {
|
||||||
DocumentFlowFormContainerActions,
|
DocumentFlowFormContainerActions,
|
||||||
@ -74,6 +78,8 @@ export const AddTemplateSettingsFormPartial = ({
|
|||||||
template,
|
template,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: AddTemplateSettingsFormProps) => {
|
}: AddTemplateSettingsFormProps) => {
|
||||||
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const { documentAuthOption } = extractDocumentAuthMethods({
|
const { documentAuthOption } = extractDocumentAuthMethods({
|
||||||
documentAuth: template.authOptions,
|
documentAuth: template.authOptions,
|
||||||
});
|
});
|
||||||
@ -90,14 +96,20 @@ export const AddTemplateSettingsFormPartial = ({
|
|||||||
message: template.templateMeta?.message ?? '',
|
message: template.templateMeta?.message ?? '',
|
||||||
timezone: template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
|
timezone: template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
|
||||||
dateFormat: template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
dateFormat: template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
|
distributionMethod:
|
||||||
|
template.templateMeta?.distributionMethod || DocumentDistributionMethod.EMAIL,
|
||||||
redirectUrl: template.templateMeta?.redirectUrl ?? '',
|
redirectUrl: template.templateMeta?.redirectUrl ?? '',
|
||||||
language: template.templateMeta?.language ?? 'en',
|
language: template.templateMeta?.language ?? 'en',
|
||||||
|
emailSettings: ZDocumentEmailSettingsSchema.parse(template?.templateMeta?.emailSettings),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { stepIndex, currentStep, totalSteps, previousStep } = useStep();
|
const { stepIndex, currentStep, totalSteps, previousStep } = useStep();
|
||||||
|
|
||||||
|
const distributionMethod = form.watch('meta.distributionMethod');
|
||||||
|
const emailSettings = form.watch('meta.emailSettings');
|
||||||
|
|
||||||
// We almost always want to set the timezone to the user's local timezone to avoid confusion
|
// We almost always want to set the timezone to the user's local timezone to avoid confusion
|
||||||
// when the document is signed.
|
// when the document is signed.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -198,6 +210,77 @@ export const AddTemplateSettingsFormPartial = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="meta.distributionMethod"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex flex-row items-center">
|
||||||
|
<Trans>Document Distribution Method</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 Distribution Method</Trans>
|
||||||
|
</strong>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
This is how the document will reach the recipients once the document is
|
||||||
|
ready for signing.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
||||||
|
<li>
|
||||||
|
<Trans>
|
||||||
|
<strong>Email</strong> - The recipient will be emailed the document to
|
||||||
|
sign, approve, etc.
|
||||||
|
</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>
|
||||||
|
<strong>Links</strong> - We will generate links which you can send to
|
||||||
|
the recipients manually.
|
||||||
|
</Trans>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Trans>
|
||||||
|
<strong>Note</strong> - If you use Links in combination with direct
|
||||||
|
templates, you will need to manually send the links to the remaining
|
||||||
|
recipients.
|
||||||
|
</Trans>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Select {...field} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger className="bg-background text-muted-foreground">
|
||||||
|
<SelectValue data-testid="documentDistributionMethodSelectValue" />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent position="popper">
|
||||||
|
{Object.values(DOCUMENT_DISTRIBUTION_METHODS).map(
|
||||||
|
({ value, description }) => (
|
||||||
|
<SelectItem key={value} value={value}>
|
||||||
|
{_(description)}
|
||||||
|
</SelectItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{isEnterprise && (
|
{isEnterprise && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@ -217,6 +300,7 @@ export const AddTemplateSettingsFormPartial = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{distributionMethod === DocumentDistributionMethod.EMAIL && (
|
||||||
<Accordion type="multiple">
|
<Accordion type="multiple">
|
||||||
<AccordionItem value="email-options" className="border-none">
|
<AccordionItem value="email-options" className="border-none">
|
||||||
<AccordionTrigger className="text-foreground rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
|
<AccordionTrigger className="text-foreground rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
|
||||||
@ -266,10 +350,16 @@ export const AddTemplateSettingsFormPartial = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<DocumentSendEmailMessageHelper />
|
<DocumentSendEmailMessageHelper />
|
||||||
|
|
||||||
|
<DocumentEmailCheckboxes
|
||||||
|
value={emailSettings}
|
||||||
|
onChange={(value) => form.setValue('meta.emailSettings', value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
)}
|
||||||
|
|
||||||
<Accordion type="multiple">
|
<Accordion type="multiple">
|
||||||
<AccordionItem value="advanced-options" className="border-none">
|
<AccordionItem value="advanced-options" className="border-none">
|
||||||
|
|||||||
@ -7,9 +7,11 @@ import {
|
|||||||
ZDocumentAccessAuthTypesSchema,
|
ZDocumentAccessAuthTypesSchema,
|
||||||
ZDocumentActionAuthTypesSchema,
|
ZDocumentActionAuthTypesSchema,
|
||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||||
|
|
||||||
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
|
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
|
||||||
|
import { DocumentDistributionMethod } from '.prisma/client';
|
||||||
|
|
||||||
export const ZAddTemplateSettingsFormSchema = z.object({
|
export const ZAddTemplateSettingsFormSchema = z.object({
|
||||||
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
||||||
@ -25,6 +27,10 @@ export const ZAddTemplateSettingsFormSchema = z.object({
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||||
dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||||
|
distributionMethod: z
|
||||||
|
.nativeEnum(DocumentDistributionMethod)
|
||||||
|
.optional()
|
||||||
|
.default(DocumentDistributionMethod.EMAIL),
|
||||||
redirectUrl: z
|
redirectUrl: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@ -36,6 +42,7 @@ export const ZAddTemplateSettingsFormSchema = z.object({
|
|||||||
.union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)])
|
.union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)])
|
||||||
.optional()
|
.optional()
|
||||||
.default('en'),
|
.default('en'),
|
||||||
|
emailSettings: ZDocumentEmailSettingsSchema,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user