diff --git a/apps/web/src/components/forms/edit-document/add-subject.action.ts b/apps/web/src/components/forms/edit-document/add-subject.action.ts index b6ff1c320..14ddef867 100644 --- a/apps/web/src/components/forms/edit-document/add-subject.action.ts +++ b/apps/web/src/components/forms/edit-document/add-subject.action.ts @@ -1,6 +1,7 @@ 'use server'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta'; import { sendDocument } from '@documenso/lib/server-only/document/send-document'; import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types'; @@ -8,12 +9,20 @@ export type CompleteDocumentActionInput = TAddSubjectFormSchema & { documentId: number; }; -export const completeDocument = async ({ documentId }: CompleteDocumentActionInput) => { +export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => { 'use server'; const { id: userId } = await getRequiredServerComponentSession(); - await sendDocument({ + if (email.message || email.subject) { + await upsertDocumentMeta({ + documentId, + subject: email.subject, + message: email.message, + }); + } + + return await sendDocument({ userId, documentId, }); diff --git a/packages/email/template-components/template-document-completed.tsx b/packages/email/template-components/template-document-completed.tsx index b64b13cff..91d8fa29d 100644 --- a/packages/email/template-components/template-document-completed.tsx +++ b/packages/email/template-components/template-document-completed.tsx @@ -1,4 +1,4 @@ -import { Button, Img, Section, Tailwind, Text } from '@react-email/components'; +import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components'; import * as config from '@documenso/tailwind-config'; @@ -29,11 +29,23 @@ export const TemplateDocumentCompleted = ({ }, }} > -
-
- Documenso -
+
+ + + + Documenso + + + + +
+ +
Completed diff --git a/packages/email/template-components/template-document-invite.tsx b/packages/email/template-components/template-document-invite.tsx index bf2fb905e..fcfba406d 100644 --- a/packages/email/template-components/template-document-invite.tsx +++ b/packages/email/template-components/template-document-invite.tsx @@ -1,4 +1,4 @@ -import { Button, Img, Section, Tailwind, Text } from '@react-email/components'; +import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components'; import * as config from '@documenso/tailwind-config'; @@ -30,13 +30,26 @@ export const TemplateDocumentInvite = ({ }, }} > -
-
- Documenso -
+
+ + + + Documenso + + + + +
+ +
- {inviterName} has invited you to sign "{documentName}" + {inviterName} has invited you to sign +
"{documentName}"
diff --git a/packages/email/template-components/template-document-pending.tsx b/packages/email/template-components/template-document-pending.tsx index 80387b783..f9fc8648a 100644 --- a/packages/email/template-components/template-document-pending.tsx +++ b/packages/email/template-components/template-document-pending.tsx @@ -1,4 +1,4 @@ -import { Img, Section, Tailwind, Text } from '@react-email/components'; +import { Column, Img, Row, Section, Tailwind, Text } from '@react-email/components'; import * as config from '@documenso/tailwind-config'; @@ -25,11 +25,23 @@ export const TemplateDocumentPending = ({ }, }} > -
-
- Documenso -
+
+ + + + Documenso + + + + +
+ +
Waiting for others diff --git a/packages/email/templates/document-invite.tsx b/packages/email/templates/document-invite.tsx index 465685649..661a2fd5f 100644 --- a/packages/email/templates/document-invite.tsx +++ b/packages/email/templates/document-invite.tsx @@ -20,7 +20,9 @@ import { } from '../template-components/template-document-invite'; import TemplateFooter from '../template-components/template-footer'; -export type DocumentInviteEmailTemplateProps = Partial; +export type DocumentInviteEmailTemplateProps = Partial & { + customBody?: string; +}; export const DocumentInviteEmailTemplate = ({ inviterName = 'Lucas Smith', @@ -28,6 +30,7 @@ export const DocumentInviteEmailTemplate = ({ documentName = 'Open Source Pledge.pdf', signDocumentLink = 'https://documenso.com', assetBaseUrl = 'http://localhost:3002', + customBody, }: DocumentInviteEmailTemplateProps) => { const previewText = `Completed Document`; @@ -78,7 +81,11 @@ export const DocumentInviteEmailTemplate = ({ - {inviterName} has invited you to sign the document "{documentName}". + {customBody ? ( +
{customBody}
+ ) : ( + `${inviterName} has invited you to sign the document "${documentName}".` + )}
diff --git a/packages/lib/server-only/document-meta/upsert-document-meta.ts b/packages/lib/server-only/document-meta/upsert-document-meta.ts new file mode 100644 index 000000000..e3cce2ea2 --- /dev/null +++ b/packages/lib/server-only/document-meta/upsert-document-meta.ts @@ -0,0 +1,30 @@ +'use server'; + +import { prisma } from '@documenso/prisma'; + +export type CreateDocumentMetaOptions = { + documentId: number; + subject: string; + message: string; +}; + +export const upsertDocumentMeta = async ({ + subject, + message, + documentId, +}: CreateDocumentMetaOptions) => { + return await prisma.documentMeta.upsert({ + where: { + documentId, + }, + create: { + subject, + message, + documentId, + }, + update: { + subject, + message, + }, + }); +}; diff --git a/packages/lib/server-only/document/get-document-by-id.ts b/packages/lib/server-only/document/get-document-by-id.ts index 0fce1af4d..0b599a71c 100644 --- a/packages/lib/server-only/document/get-document-by-id.ts +++ b/packages/lib/server-only/document/get-document-by-id.ts @@ -13,6 +13,7 @@ export const getDocumentById = async ({ id, userId }: GetDocumentByIdOptions) => }, include: { documentData: true, + documentMeta: true, }, }); }; diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index 83a88f24a..fcc0f829c 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -3,13 +3,14 @@ import { createElement } from 'react'; import { mailer } from '@documenso/email/mailer'; import { render } from '@documenso/email/render'; import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite'; +import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template'; import { prisma } from '@documenso/prisma'; import { DocumentStatus, SendStatus } from '@documenso/prisma/client'; -export interface SendDocumentOptions { +export type SendDocumentOptions = { documentId: number; userId: number; -} +}; export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) => { const user = await prisma.user.findFirstOrThrow({ @@ -25,9 +26,12 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) }, include: { Recipient: true, + documentMeta: true, }, }); + const customEmail = document?.documentMeta; + if (!document) { throw new Error('Document not found'); } @@ -44,6 +48,12 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) document.Recipient.map(async (recipient) => { const { email, name } = recipient; + const customEmailTemplate = { + 'signer.name': name, + 'signer.email': email, + 'document.name': document.title, + }; + if (recipient.sendStatus === SendStatus.SENT) { return; } @@ -57,6 +67,7 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) inviterEmail: user.email, assetBaseUrl, signDocumentLink, + customBody: renderCustomEmailTemplate(customEmail?.message || '', customEmailTemplate), }); await mailer.sendMail({ @@ -68,7 +79,9 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', }, - subject: 'Please sign this document', + subject: customEmail?.subject + ? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate) + : 'Please sign this document', html: render(template), text: render(template, { plainText: true }), }); diff --git a/packages/lib/server-only/document/update-document.ts b/packages/lib/server-only/document/update-document.ts new file mode 100644 index 000000000..7793c990a --- /dev/null +++ b/packages/lib/server-only/document/update-document.ts @@ -0,0 +1,21 @@ +'use server'; + +import { Prisma } from '@prisma/client'; + +import { prisma } from '@documenso/prisma'; + +export type UpdateDocumentOptions = { + documentId: number; + data: Prisma.DocumentUpdateInput; +}; + +export const updateDocument = async ({ documentId, data }: UpdateDocumentOptions) => { + return await prisma.document.update({ + where: { + id: documentId, + }, + data: { + ...data, + }, + }); +}; diff --git a/packages/lib/utils/render-custom-email-template.ts b/packages/lib/utils/render-custom-email-template.ts new file mode 100644 index 000000000..e3fdf5c7b --- /dev/null +++ b/packages/lib/utils/render-custom-email-template.ts @@ -0,0 +1,12 @@ +export const renderCustomEmailTemplate = >( + template: string, + variables: T, +): string => { + return template.replace(/\{(\S+)\}/g, (_, key) => { + if (key in variables) { + return variables[key]; + } + + return key; + }); +}; diff --git a/packages/prisma/migrations/20230920052232_document_meta/migration.sql b/packages/prisma/migrations/20230920052232_document_meta/migration.sql new file mode 100644 index 000000000..00e9db735 --- /dev/null +++ b/packages/prisma/migrations/20230920052232_document_meta/migration.sql @@ -0,0 +1,14 @@ +-- AlterTable +ALTER TABLE "Document" ADD COLUMN "documentMetaId" TEXT; + +-- CreateTable +CREATE TABLE "DocumentMeta" ( + "id" TEXT NOT NULL, + "customEmailSubject" TEXT, + "customEmailBody" TEXT, + + CONSTRAINT "DocumentMeta_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_documentMetaId_fkey" FOREIGN KEY ("documentMetaId") REFERENCES "DocumentMeta"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20230920124941_fix_documentmeta_relation/migration.sql b/packages/prisma/migrations/20230920124941_fix_documentmeta_relation/migration.sql new file mode 100644 index 000000000..69b4591c5 --- /dev/null +++ b/packages/prisma/migrations/20230920124941_fix_documentmeta_relation/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[documentMetaId]` on the table `Document` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Document_documentMetaId_key" ON "Document"("documentMetaId"); diff --git a/packages/prisma/migrations/20230922121421_fix_document_meta_schema/migration.sql b/packages/prisma/migrations/20230922121421_fix_document_meta_schema/migration.sql new file mode 100644 index 000000000..42c20c112 --- /dev/null +++ b/packages/prisma/migrations/20230922121421_fix_document_meta_schema/migration.sql @@ -0,0 +1,52 @@ +/* + Warnings: + + - You are about to drop the column `documentMetaId` on the `Document` table. All the data in the column will be lost. + - You are about to drop the column `customEmailBody` on the `DocumentMeta` table. All the data in the column will be lost. + - You are about to drop the column `customEmailSubject` on the `DocumentMeta` table. All the data in the column will be lost. + - A unique constraint covering the columns `[documentId]` on the table `DocumentMeta` will be added. If there are existing duplicate values, this will fail. + - Added the required column `documentId` to the `DocumentMeta` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Document" DROP CONSTRAINT "Document_documentMetaId_fkey"; + +-- DropIndex +DROP INDEX "Document_documentMetaId_key"; + +-- AlterTable +ALTER TABLE "DocumentMeta" +ADD COLUMN "documentId" INTEGER, +ADD COLUMN "message" TEXT, +ADD COLUMN "subject" TEXT; + +-- Migrate data +UPDATE "DocumentMeta" SET "documentId" = ( + SELECT "id" FROM "Document" WHERE "Document"."documentMetaId" = "DocumentMeta"."id" +); + +-- Migrate data +UPDATE "DocumentMeta" SET "message" = "customEmailBody"; + +-- Migrate data +UPDATE "DocumentMeta" SET "subject" = "customEmailSubject"; + +-- Prune data +DELETE FROM "DocumentMeta" WHERE "documentId" IS NULL; + +-- AlterTable +ALTER TABLE "Document" DROP COLUMN "documentMetaId"; + +-- AlterTable +ALTER TABLE "DocumentMeta" +DROP COLUMN "customEmailBody", +DROP COLUMN "customEmailSubject"; + +-- AlterColumn +ALTER TABLE "DocumentMeta" ALTER COLUMN "documentId" SET NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "DocumentMeta_documentId_key" ON "DocumentMeta"("documentId"); + +-- AddForeignKey +ALTER TABLE "DocumentMeta" ADD CONSTRAINT "DocumentMeta_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 96b7db0a3..6576da8e2 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -110,6 +110,7 @@ model Document { Field Field[] documentDataId String documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade) + documentMeta DocumentMeta? createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt @@ -130,6 +131,14 @@ model DocumentData { Document Document? } +model DocumentMeta { + id String @id @default(cuid()) + subject String? + message String? + documentId Int @unique + document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) +} + enum ReadStatus { NOT_OPENED OPENED diff --git a/packages/prisma/types/document-with-data.ts b/packages/prisma/types/document-with-data.ts index d52987552..d8dd8a888 100644 --- a/packages/prisma/types/document-with-data.ts +++ b/packages/prisma/types/document-with-data.ts @@ -1,5 +1,6 @@ -import { Document, DocumentData } from '@documenso/prisma/client'; +import { Document, DocumentData, DocumentMeta } from '@documenso/prisma/client'; export type DocumentWithData = Document & { documentData?: DocumentData | null; + documentMeta?: DocumentMeta | null; }; diff --git a/packages/ui/primitives/document-flow/add-subject.tsx b/packages/ui/primitives/document-flow/add-subject.tsx index 312a4eea2..1bf3b2cb4 100644 --- a/packages/ui/primitives/document-flow/add-subject.tsx +++ b/packages/ui/primitives/document-flow/add-subject.tsx @@ -2,7 +2,8 @@ import { useForm } from 'react-hook-form'; -import { Document, DocumentStatus, Field, Recipient } from '@documenso/prisma/client'; +import { DocumentStatus, Field, Recipient } from '@documenso/prisma/client'; +import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; @@ -21,7 +22,7 @@ export type AddSubjectFormProps = { documentFlow: DocumentFlowStep; recipients: Recipient[]; fields: Field[]; - document: Document; + document: DocumentWithData; numberOfSteps: number; onSubmit: (_data: TAddSubjectFormSchema) => void; }; @@ -41,8 +42,8 @@ export const AddSubjectFormPartial = ({ } = useForm({ defaultValues: { email: { - subject: '', - message: '', + subject: document.documentMeta?.subject ?? '', + message: document.documentMeta?.message ?? '', }, }, });