diff --git a/apps/marketing/src/app/not-found.tsx b/apps/marketing/src/app/not-found.tsx new file mode 100644 index 000000000..9f87cdc88 --- /dev/null +++ b/apps/marketing/src/app/not-found.tsx @@ -0,0 +1,65 @@ +'use client'; + +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; + +import { motion } from 'framer-motion'; +import { ChevronLeft } from 'lucide-react'; + +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; + +import backgroundPattern from '~/assets/background-pattern.png'; + +export default function NotFound() { + const router = useRouter(); + + return ( +
+
+ + background pattern + +
+ +
+
+

404 Page not found

+ +

Oops! Something went wrong.

+ +

+ The page you are looking for was moved, removed, renamed or might never have existed. +

+ +
+ + + +
+
+
+
+ ); +} diff --git a/apps/marketing/src/app/robots.ts b/apps/marketing/src/app/robots.ts index de74ea314..cc718ff25 100644 --- a/apps/marketing/src/app/robots.ts +++ b/apps/marketing/src/app/robots.ts @@ -4,11 +4,11 @@ import { getBaseUrl } from '@documenso/lib/universal/get-base-url'; export default function robots(): MetadataRoute.Robots { return { - rules: { - userAgent: '*', - allow: '/*', - disallow: ['/_next/*'], - }, + rules: [ + { + userAgent: '*', + }, + ], sitemap: `${getBaseUrl()}/sitemap.xml`, }; } diff --git a/apps/web/src/app/(dashboard)/documents/empty-state.tsx b/apps/web/src/app/(dashboard)/documents/empty-state.tsx new file mode 100644 index 000000000..bdfef3bbe --- /dev/null +++ b/apps/web/src/app/(dashboard)/documents/empty-state.tsx @@ -0,0 +1,50 @@ +import { Bird, CheckCircle2 } from 'lucide-react'; +import { match } from 'ts-pattern'; + +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; + +export type EmptyDocumentProps = { status: ExtendedDocumentStatus }; + +export const EmptyDocumentState = ({ status }: EmptyDocumentProps) => { + const { + title, + message, + icon: Icon, + } = match(status) + .with(ExtendedDocumentStatus.COMPLETED, () => ({ + title: 'Nothing to do', + message: + 'There are no completed documents yet. Documents that you have created or received that become completed will appear here later.', + icon: CheckCircle2, + })) + .with(ExtendedDocumentStatus.DRAFT, () => ({ + title: 'No active drafts', + message: + 'There are no active drafts at then current moment. You can upload a document to start drafting.', + icon: CheckCircle2, + })) + .with(ExtendedDocumentStatus.ALL, () => ({ + title: "We're all empty", + message: + 'You have not yet created or received any documents. To create a document please upload one.', + icon: Bird, + })) + .otherwise(() => ({ + title: 'Nothing to do', + message: + 'All documents are currently actioned. Any new documents are sent or recieved they will start to appear here.', + icon: CheckCircle2, + })); + + return ( +
+ + +
+

{title}

+ +

{message}

+
+
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx index 32ec4f814..9ae16e44b 100644 --- a/apps/web/src/app/(dashboard)/documents/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/page.tsx @@ -12,6 +12,7 @@ import { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/ty import { DocumentStatus } from '~/components/formatter/document-status'; import { DocumentsDataTable } from './data-table'; +import { EmptyDocumentState } from './empty-state'; import { UploadDocument } from './upload-document'; export type DocumentsPageProps = { @@ -96,7 +97,8 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
- + {results.count > 0 && } + {results.count === 0 && }
); diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx new file mode 100644 index 000000000..c8dc15086 --- /dev/null +++ b/apps/web/src/app/not-found.tsx @@ -0,0 +1,26 @@ +import Link from 'next/link'; + +import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { Button } from '@documenso/ui/primitives/button'; + +import NotFoundPartial from '~/components/partials/not-found'; + +export default async function NotFound() { + const session = await getServerComponentSession(); + + return ( + + {session && ( + + )} + + {!session && ( + + )} + + ); +} 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/apps/web/src/components/motion.tsx b/apps/web/src/components/motion.tsx deleted file mode 100644 index 2e9d19eae..000000000 --- a/apps/web/src/components/motion.tsx +++ /dev/null @@ -1,7 +0,0 @@ -'use client'; - -import { motion } from 'framer-motion'; - -export * from 'framer-motion'; - -export const MotionDiv = motion.div; diff --git a/apps/web/src/components/partials/not-found.tsx b/apps/web/src/components/partials/not-found.tsx new file mode 100644 index 000000000..0b5c2ad18 --- /dev/null +++ b/apps/web/src/components/partials/not-found.tsx @@ -0,0 +1,66 @@ +'use client'; + +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; + +import { motion } from 'framer-motion'; +import { ChevronLeft } from 'lucide-react'; + +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; + +import backgroundPattern from '~/assets/background-pattern.png'; + +export type NotFoundPartialProps = { + children?: React.ReactNode; +}; + +export default function NotFoundPartial({ children }: NotFoundPartialProps) { + const router = useRouter(); + + return ( +
+
+ + background pattern + +
+ +
+
+

404 Page not found

+ +

Oops! Something went wrong.

+ +

+ The page you are looking for was moved, removed, renamed or might never have existed. +

+ +
+ + + {children} +
+
+
+
+ ); +} 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/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts index a013d5e69..2712c56fa 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -4,6 +4,7 @@ import { prisma } from '@documenso/prisma'; import { DocumentStatus, SigningStatus } from '@documenso/prisma/client'; import { sealDocument } from './seal-document'; +import { sendPendingEmail } from './send-pending-email'; export type CompleteDocumentWithTokenOptions = { token: string; @@ -69,6 +70,19 @@ export const completeDocumentWithToken = async ({ }, }); + const pendingRecipients = await prisma.recipient.count({ + where: { + documentId: document.id, + signingStatus: { + not: SigningStatus.SIGNED, + }, + }, + }); + + if (pendingRecipients > 0) { + await sendPendingEmail({ documentId, recipientId: recipient.id }); + } + const documents = await prisma.document.updateMany({ where: { id: document.id, 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/seal-document.ts b/packages/lib/server-only/document/seal-document.ts index 883d13e6f..d551e4adf 100644 --- a/packages/lib/server-only/document/seal-document.ts +++ b/packages/lib/server-only/document/seal-document.ts @@ -9,6 +9,7 @@ import { DocumentStatus, SigningStatus } from '@documenso/prisma/client'; import { getFile } from '../../universal/upload/get-file'; import { putFile } from '../../universal/upload/put-file'; import { insertFieldInPDF } from '../pdf/insert-field-in-pdf'; +import { sendCompletedEmail } from './send-completed-email'; export type SealDocumentOptions = { documentId: number; @@ -86,4 +87,6 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => { data: newData, }, }); + + await sendCompletedEmail({ documentId }); }; diff --git a/packages/lib/server-only/document/send-completed-email.ts b/packages/lib/server-only/document/send-completed-email.ts new file mode 100644 index 000000000..9d0d2d499 --- /dev/null +++ b/packages/lib/server-only/document/send-completed-email.ts @@ -0,0 +1,57 @@ +import { createElement } from 'react'; + +import { mailer } from '@documenso/email/mailer'; +import { render } from '@documenso/email/render'; +import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed'; +import { prisma } from '@documenso/prisma'; + +export interface SendDocumentOptions { + documentId: number; +} + +export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) => { + const document = await prisma.document.findUnique({ + where: { + id: documentId, + }, + include: { + Recipient: true, + }, + }); + + if (!document) { + throw new Error('Document not found'); + } + + if (document.Recipient.length === 0) { + throw new Error('Document has no recipients'); + } + + await Promise.all([ + document.Recipient.map(async (recipient) => { + const { email, name, token } = recipient; + + const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; + + const template = createElement(DocumentCompletedEmailTemplate, { + documentName: document.title, + assetBaseUrl, + downloadLink: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${token}/complete`, + }); + + await mailer.sendMail({ + to: { + address: email, + name, + }, + from: { + name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', + address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', + }, + subject: 'Signing Complete!', + html: render(template), + text: render(template, { plainText: 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/send-pending-email.ts b/packages/lib/server-only/document/send-pending-email.ts new file mode 100644 index 000000000..75861be78 --- /dev/null +++ b/packages/lib/server-only/document/send-pending-email.ts @@ -0,0 +1,64 @@ +import { createElement } from 'react'; + +import { mailer } from '@documenso/email/mailer'; +import { render } from '@documenso/email/render'; +import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending'; +import { prisma } from '@documenso/prisma'; + +export interface SendPendingEmailOptions { + documentId: number; + recipientId: number; +} + +export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingEmailOptions) => { + const document = await prisma.document.findFirst({ + where: { + id: documentId, + Recipient: { + some: { + id: recipientId, + }, + }, + }, + include: { + Recipient: { + where: { + id: recipientId, + }, + }, + }, + }); + + if (!document) { + throw new Error('Document not found'); + } + + if (document.Recipient.length === 0) { + throw new Error('Document has no recipients'); + } + + const [recipient] = document.Recipient; + + const { email, name } = recipient; + + const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; + + const template = createElement(DocumentPendingEmailTemplate, { + documentName: document.title, + assetBaseUrl, + }); + + await mailer.sendMail({ + to: { + address: email, + name, + }, + from: { + name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', + address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', + }, + subject: 'Waiting for others to complete signing.', + 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/server-only/pdf/insert-field-in-pdf.ts b/packages/lib/server-only/pdf/insert-field-in-pdf.ts index 61726f53c..e7b1e7c5a 100644 --- a/packages/lib/server-only/pdf/insert-field-in-pdf.ts +++ b/packages/lib/server-only/pdf/insert-field-in-pdf.ts @@ -50,10 +50,10 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu let imageWidth = image.width; let imageHeight = image.height; - const initialDimensions = { - width: imageWidth, - height: imageHeight, - }; + // const initialDimensions = { + // width: imageWidth, + // height: imageHeight, + // }; const scalingFactor = Math.min(fieldWidth / imageWidth, fieldHeight / imageHeight, 1); @@ -76,10 +76,10 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu let textWidth = font.widthOfTextAtSize(field.customText, fontSize); const textHeight = font.heightAtSize(fontSize); - const initialDimensions = { - width: textWidth, - height: textHeight, - }; + // const initialDimensions = { + // width: textWidth, + // height: textHeight, + // }; const scalingFactor = Math.min(fieldWidth / textWidth, fieldHeight / textHeight, 1); 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 87321e20f..c4f034ba2 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -110,9 +110,10 @@ model Document { Field Field[] ShareLink DocumentShareLink[] documentDataId String - documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade) - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt + documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade) + documentMeta DocumentMeta? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt @@unique([documentDataId]) } @@ -131,6 +132,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 ?? '', }, }, });