Compare commits

..

9 Commits

Author SHA1 Message Date
Mythie f4c7799537 fix: reverse meta relation and tidy code 2023-09-22 12:42:06 +00:00
Ephraim Atta-Duncan e330e90688 fix: document meta relation with document 2023-09-20 12:54:24 +00:00
Ephraim Atta-Duncan f2d3c51651 fix: avoid creating document meta with empty strings 2023-09-20 12:38:39 +00:00
Ephraim Atta-Duncan c9c111cdf2 fix: persist newline in emails 2023-09-20 12:19:52 +00:00
Ephraim Atta-Duncan d417255910 feat: replace template variables with values
Co-authored-by: Mythie <me@lucasjamessmith.me>
2023-09-20 11:17:26 +00:00
Ephraim Atta-Duncan 677a15327b feat: send custom email message 2023-09-20 09:59:42 +00:00
Ephraim Atta-Duncan d5238939ad feat: persist document metadata in database for a specific user 2023-09-20 09:51:04 +00:00
Ephraim Atta-Duncan 6860726e83 feat: send custom email subjects 2023-09-20 09:06:28 +00:00
Ephraim Atta-Duncan a55197fb2d feat: add prisma schema for document meta 2023-09-20 08:57:50 +00:00
20 changed files with 243 additions and 129 deletions
-101
View File
@@ -1,101 +0,0 @@
---
title: A week of announcements
description: An overview of what's new at Documenso.
authorName: 'Flo Merian'
authorImage: '/blog/blog-author-flo.jpeg'
authorRole: 'Go-to-market'
date: 2023-09-20
tags:
- Announcement
- Community
---
We just spent the week announcing something new every day. This blog post gives an overview of what's new at Documenso.
## A week of announcements
### Day 1: Documenso Design System
<figure>
<MdxNextImage
src="/blog/blog-fig-launch-week-documenso-design-system.webp"
width="2000"
height="1126"
alt="Documenso Design System"
/>
<figcaption className="text-center">Documenso Design System</figcaption>
</figure>
We open-sourced Documenso's design system.
Documenso isn't just an open-source alternative to DocuSign. It's a beautiful document signing experience.
We welcome every developer AND designer to contribute to building the product.
[This Figma file]() serves as an open source guide for the design of the product across website, desktop & mobile apps, and brand.
It includes design tokens, primitives and components, screens, and brand assets.
Go duplicate and remix it!
### Day 2: Malfunction Mania
Documenso 1.0 just hit the testing environment.
We want to ensure the best possible release. That's why we started this week a public testing phase, the most significant community project yet, where we invite you to try out the new version, report and fix bugs and give feedback before release.
We call it "Malfunction Mania."
Early contributors love it so far:
"I love the refresh. The UI is much cleaner and the possibility to select the signature spot, date, name, email, and change the size, is a life-saver..."
Documenso's co-founder and CEO Timur shared the details in [this announcement]().
### Day 3: Documenso Shop
Supporting and contributing to open-source projects like Documenso is cool for sure.
Do you know what's *even* cooler tho? To wear swag from open-source projects.
We announced the [Documenso Shop](https://documen.so/shop), a place where you can buy some merch. Suit up!
### Day 4: Early Adopter Plan
<figure>
<MdxNextImage
src="/blog/blog-fig-launch-week-early-adopter-plan.webp"
width="2000"
height="1126"
alt="Early Adopter Plan"
/>
<figcaption className="text-center">The Early Adopter Plan. $30/month, forever. Available for the first 100 new signups.</figcaption>
</figure>
As we ramp up development speed, we introduced the [Early Adopter Plan](https://documen.so/pricing), a special, fixed $30/mo offer for 100 early adopters who want to get deep hands-on feedback.
Timur published a blog post [here]() to explain how we plan to build the core version of [documenso.com](http://documenso.com/).
### Day 5: Upcoming Launches
<figure>
<MdxNextImage
src="/blog/blog-fig-launch-week-upcoming-launches.webp"
width="2000"
height="1126"
alt="Documenso Upcoming Launches"
/>
</figure>
Say hello! hey! yo! to the public roadmap.
Go to the repository to find it and preview what's next at Documenso, the features we're working on and upcoming launches.
Make sure to [star the repo on GitHub](https://documen.so/github) and watch it to get the latest updates.
## Wrapping up
So this week was Documenso's first Launch Week. We hope you enjoyed it as much as we did.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

@@ -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,
});
@@ -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 = ({
},
}}
>
<Section className="flex-row items-center justify-center">
<div className="flex items-center justify-center p-4">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</div>
<Section>
<Row className="table-fixed">
<Column />
<Column>
<Img
className="h-42 mx-auto"
src={getAssetUrl('/static/document.png')}
alt="Documenso"
/>
</Column>
<Column />
</Row>
</Section>
<Section>
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
Completed
@@ -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 = ({
},
}}
>
<Section className="mt-4 flex-row items-center justify-center">
<div className="flex items-center justify-center p-4">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</div>
<Section className="mt-4">
<Row className="table-fixed">
<Column />
<Column>
<Img
className="h-42 mx-auto"
src={getAssetUrl('/static/document.png')}
alt="Documenso"
/>
</Column>
<Column />
</Row>
</Section>
<Section>
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
{inviterName} has invited you to sign "{documentName}"
{inviterName} has invited you to sign
<br />"{documentName}"
</Text>
<Text className="my-1 text-center text-base text-slate-400">
@@ -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 = ({
},
}}
>
<Section className="flex-row items-center justify-center">
<div className="flex items-center justify-center p-4">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</div>
<Section>
<Row className="table-fixed">
<Column />
<Column>
<Img
className="h-42 mx-auto"
src={getAssetUrl('/static/document.png')}
alt="Documenso"
/>
</Column>
<Column />
</Row>
</Section>
<Section>
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-blue-500">
<Img src={getAssetUrl('/static/clock.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
Waiting for others
+9 -2
View File
@@ -20,7 +20,9 @@ import {
} from '../template-components/template-document-invite';
import TemplateFooter from '../template-components/template-footer';
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps>;
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
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 = ({
</Text>
<Text className="mt-2 text-base text-slate-400">
{inviterName} has invited you to sign the document "{documentName}".
{customBody ? (
<pre className="font-sans text-base text-slate-400">{customBody}</pre>
) : (
`${inviterName} has invited you to sign the document "${documentName}".`
)}
</Text>
</Section>
</Container>
@@ -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,
},
});
};
@@ -13,6 +13,7 @@ export const getDocumentById = async ({ id, userId }: GetDocumentByIdOptions) =>
},
include: {
documentData: true,
documentMeta: true,
},
});
};
@@ -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 }),
});
@@ -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,
},
});
};
@@ -0,0 +1,12 @@
export const renderCustomEmailTemplate = <T extends Record<string, string>>(
template: string,
variables: T,
): string => {
return template.replace(/\{(\S+)\}/g, (_, key) => {
if (key in variables) {
return variables[key];
}
return key;
});
};
@@ -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;
@@ -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");
@@ -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;
+9
View File
@@ -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
+2 -1
View File
@@ -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;
};
@@ -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<TAddSubjectFormSchema>({
defaultValues: {
email: {
subject: '',
message: '',
subject: document.documentMeta?.subject ?? '',
message: document.documentMeta?.message ?? '',
},
},
});