Compare commits
23 Commits
feat/reset
...
feat/custo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4c7799537 | ||
|
|
e330e90688 | ||
|
|
f2d3c51651 | ||
|
|
c9c111cdf2 | ||
|
|
d417255910 | ||
|
|
677a15327b | ||
|
|
d5238939ad | ||
|
|
6860726e83 | ||
|
|
a55197fb2d | ||
|
|
e6e8de62c8 | ||
|
|
71c7a6ee8c | ||
|
|
ecc8e59c8c | ||
|
|
d0f027c4ea | ||
|
|
2c667f785c | ||
|
|
1adf7e183e | ||
|
|
58580c7fac | ||
|
|
d4659eee07 | ||
|
|
40274021ba | ||
|
|
3cbc722b94 | ||
|
|
8844143ff5 | ||
|
|
7de7624477 | ||
|
|
7c6b5ac59d | ||
|
|
9c45ce61b8 |
@@ -0,0 +1,198 @@
|
|||||||
|
---
|
||||||
|
title: 'Deploying Documenso with Vercel, Supabase and Resend'
|
||||||
|
description: This is the first part of the new Building Documenso series, where I describe the challenges and design choices that we make while building the world’s most open signing platform.
|
||||||
|
authorName: 'Ephraim Atta-Duncan'
|
||||||
|
authorImage: '/blog/blog-author-duncan.jpeg'
|
||||||
|
authorRole: 'Software Engineer Intern'
|
||||||
|
date: 2023-09-08
|
||||||
|
tags:
|
||||||
|
- Open Source
|
||||||
|
- Self Hosting
|
||||||
|
- Tutorial
|
||||||
|
---
|
||||||
|
|
||||||
|
In this article, we'll walk you through how to deploy and self-host Documenso using Vercel, Supabase, and Resend.
|
||||||
|
|
||||||
|
You'll learn:
|
||||||
|
|
||||||
|
- How to set up a Postgres database using Supabase,
|
||||||
|
- How to install SMTP with Resend,
|
||||||
|
- How to deploy your project with Vercel.
|
||||||
|
|
||||||
|
If you don't know what [Documenso](https://documenso.com/) is, it's an open-source alternative to DocuSign, with the mission to create an open signing infrastructure while embracing openness, cooperation, and transparency.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before we start, make sure you have a [GitHub](https://github.com/signup) account. You also need [Node.js](https://nodejs.org/en) and [npm](https://www.npmjs.com/) installed on your local machine (note: you also have the option to host it on a cloud environment using Gitpod for example; that would be another post). If you need accounts on Vercel, Supabase, and Resend, create them by visiting the [Vercel](https://vercel.com/), [Supabase](https://supabase.com/), and [Resend](https://resend.com/) websites.
|
||||||
|
|
||||||
|
Checklist:
|
||||||
|
|
||||||
|
- [ ] Have a GitHub account
|
||||||
|
- [ ] Install Node.js
|
||||||
|
- [ ] Install npm
|
||||||
|
- [ ] Have a Vercel account
|
||||||
|
- [ ] Have a Supabase account
|
||||||
|
- [ ] Have a Resend account
|
||||||
|
|
||||||
|
## Step-by-Step guide to deploying Documenso with Vercel, Supabase, and Resend
|
||||||
|
|
||||||
|
To deploy Documenso, we'll take the following steps:
|
||||||
|
|
||||||
|
1. Fork the Documenso repository
|
||||||
|
2. Clone the forked repository and install dependencies
|
||||||
|
3. Create a new project on Supabase
|
||||||
|
4. Copy the Supabase Postgres database connection URL
|
||||||
|
5. Create a `.env` file
|
||||||
|
6. Run the migration on the Supabase Postgres Database
|
||||||
|
7. Get your SMTP Keys on Resend
|
||||||
|
8. Create a new project on Vercel
|
||||||
|
9. Add Environment Variables to your Vercel project
|
||||||
|
|
||||||
|
So, you're ready? Let’s dive in!
|
||||||
|
|
||||||
|
### Step 1: Fork the Documenso repository
|
||||||
|
|
||||||
|
Start by creating a fork of Documenso on GitHub. You can do this by visiting the [Documenso repository](https://github.com/documenso/documenso) and clicking on the 'Fork' button. (Also, star the repo!)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Choose your GitHub profile as the owner and click on 'Create fork' to create a fork of the repo.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 2: Clone the forked repository and install dependencies
|
||||||
|
|
||||||
|
Clone the forked repository to your local machine in any directory of your choice. Open your terminal and enter the following commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repo using Github CLI
|
||||||
|
gh repo clone [your_github_username]/documenso
|
||||||
|
|
||||||
|
# Clone the repo using Git
|
||||||
|
git clone <https://github.com/[your_github_username]/documenso.git>
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now navigate into the directory and install the project’s dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd documenso
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Create a new project on Supabase
|
||||||
|
|
||||||
|
Now, let's set up the database.
|
||||||
|
|
||||||
|
If you haven't already, create a new project on Supabase. This will automatically create a new Postgres database for you.
|
||||||
|
|
||||||
|
On your Supabase dashboard, click the '**New project**' button and choose your organization.
|
||||||
|
|
||||||
|
On the '**Create a new project**' page, set a database name of **documenso** and a secure password for your database. Choose a region closer to you, a pricing plan, and click on '**Create new project**' to create your project.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 4: Copy the Supabase Postgres database connection URL
|
||||||
|
|
||||||
|
In your project, click the '**Settings**' icon at the bottom left.
|
||||||
|
|
||||||
|
Under the '**Project Settings**' section, click '**Database**' and scroll down to the '**Connection string**' section. Copy the '**URI**' and update it with the password you chose in the previous step.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 5: Create a `.env` file
|
||||||
|
|
||||||
|
Create a `.env` file in the root of your project by copying the contents of the `.env.example` file.
|
||||||
|
|
||||||
|
Add the connection string you copied from your Supabase dashboard to the `DATABASE_URL` variable in the `.env` file.
|
||||||
|
|
||||||
|
The `.env` should look like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Run the migration on the Supabase Postgres Database
|
||||||
|
|
||||||
|
Run the migration on the Supabase Postgres Database using the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx prisma migrate deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Get your SMTP Keys on Resend
|
||||||
|
|
||||||
|
So, you've just cloned Documenso, installed dependencies on your local machine, and set your database using Supabase. Now, SMTP is missing. Emails won't go out! Let's fix it with Resend.
|
||||||
|
|
||||||
|
In the **[Resend](https://resend.com/)** dashboard, click 'Add API Key' to create a key for Resend SMTP.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Next, add and verify your domain in the '**Domains**' section on the sidebar. This will allow you to send emails from any address associated with your domain.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can update your `.env` file with the following:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
SMTP_MAIL_HOST = 'smtp.resend.com';
|
||||||
|
SMTP_MAIL_PORT = '25';
|
||||||
|
SMTP_MAIL_USER = 'resend';
|
||||||
|
SMTP_MAIL_PASSWORD = 'YOUR_RESEND_API_KEY';
|
||||||
|
MAIL_FROM = 'noreply@[YOUR_DOMAIN]';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8: Create a new project on Vercel
|
||||||
|
|
||||||
|
You set the database with Supabase and are SMTP-ready with Resend. Almost there! The next step is to deploy the project — we'll use Vercel for that.
|
||||||
|
|
||||||
|
On your Vercel dashboard, create a new project using the forked project from your GitHub repositories. Select the project among the options and click '**Import**' to start running Documenso.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 9: Add Environment Variables to your Vercel project
|
||||||
|
|
||||||
|
In the '**Configure Project**' page, adding the required Environmental Variables is essential to ensure the application deploys without any errors.
|
||||||
|
|
||||||
|
Specifically, for the `NEXT_PUBLIC_WEBAPP_URL` and `NEXTAUTH_URL` variables, you must add `.vercel.app` to your Project Name. This will form the deployment URL, which will be in the format: `https://[project_name].vercel.app`.
|
||||||
|
|
||||||
|
For example, in my case, the deployment URL is `https://documenso-supabase-web.vercel.app`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This is a sample `.env` to deploy. Copy and paste it to auto-populate the fields and click ‘**Deploy.’** Now, you only need to wait for your project to deploy. You’re going live — enjoy!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DATABASE_URL='postgresql://postgres:typeinastrongpassword@db.njuigobjlbteahssqbtw.supabase.co:5432/postgres'
|
||||||
|
|
||||||
|
NEXT_PUBLIC_WEBAPP_URL='https://documenso-supabase-web.vercel.app'
|
||||||
|
NEXTAUTH_SECRET='something gibrish to encrypt your jwt tokens'
|
||||||
|
NEXTAUTH_URL='https://documenso-supabase-web.vercel.app'
|
||||||
|
|
||||||
|
# Get a Sendgrid Api key here: <https://signup.sendgrid.com>
|
||||||
|
SENDGRID_API_KEY=''
|
||||||
|
|
||||||
|
# Set SMTP credentials to use SMTP instead of the Sendgrid API.
|
||||||
|
SMTP_MAIL_HOST='smtp.resend.com'
|
||||||
|
SMTP_MAIL_PORT='25'
|
||||||
|
SMTP_MAIL_USER='resend'
|
||||||
|
SMTP_MAIL_PASSWORD='YOUR_RESEND_API_KEY'
|
||||||
|
MAIL_FROM='noreply@[YOUR_DOMAIN]'
|
||||||
|
|
||||||
|
NEXT_PUBLIC_ALLOW_SIGNUP=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wrapping up
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Congratulations! 🎉 You've successfully deployed Documenso using Vercel, Supabase, and Resend. You're now ready to create and sign your own documents with your self-hosted Documenso!
|
||||||
|
|
||||||
|
In this step-by-step guide, you learned how to:
|
||||||
|
|
||||||
|
- set up a Postgres database using Supabase,
|
||||||
|
- install SMTP with Resend,
|
||||||
|
- deploy your project with Vercel.
|
||||||
|
|
||||||
|
Over to you! How was the tutorial? If you enjoyed it, [please do share](https://twitter.com/documenso/status/1700141802693480482)! And if you have any questions or comments, please reach out to me on [Twitter / X](https://twitter.com/EphraimDuncan_) (DM open) or [Discord](https://documen.so/discord).
|
||||||
|
|
||||||
|
We're building an open-source alternative to DocuSign and welcome every contribution. Head over to the GitHub repository and [leave us a Star](https://github.com/documenso/documenso)!
|
||||||
BIN
apps/marketing/public/blog/blog-author-duncan.jpeg
Normal file
BIN
apps/marketing/public/blog/blog-author-duncan.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -68,7 +68,7 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
|
|||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
|
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
|
||||||
<Loader className="h-12 w-12 animate-spin text-slate-500" />
|
<Loader className="text-muted-foreground h-12 w-12 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default async function BillingSettingsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium">Billing</h3>
|
<h3 className="text-lg font-medium">Billing</h3>
|
||||||
|
|
||||||
<p className="mt-2 text-sm text-slate-500">
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
Your subscription is{' '}
|
Your subscription is{' '}
|
||||||
{subscription.status !== SubscriptionStatus.INACTIVE ? 'active' : 'inactive'}.
|
{subscription.status !== SubscriptionStatus.INACTIVE ? 'active' : 'inactive'}.
|
||||||
{subscription?.periodEnd && (
|
{subscription?.periodEnd && (
|
||||||
@@ -67,7 +67,7 @@ export default async function BillingSettingsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!billingPortalUrl && (
|
{!billingPortalUrl && (
|
||||||
<p className="max-w-[60ch] text-base text-slate-500">
|
<p className="text-muted-foreground max-w-[60ch] text-base">
|
||||||
You do not currently have a customer record, this should not happen. Please contact
|
You do not currently have a customer record, this should not happen. Please contact
|
||||||
support for assistance.
|
support for assistance.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default async function PasswordSettingsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium">Password</h3>
|
<h3 className="text-lg font-medium">Password</h3>
|
||||||
|
|
||||||
<p className="mt-2 text-sm text-slate-500">Here you can update your password.</p>
|
<p className="text-muted-foreground mt-2 text-sm">Here you can update your password.</p>
|
||||||
|
|
||||||
<hr className="my-4" />
|
<hr className="my-4" />
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default async function ProfileSettingsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium">Profile</h3>
|
<h3 className="text-lg font-medium">Profile</h3>
|
||||||
|
|
||||||
<p className="mt-2 text-sm text-slate-500">Here you can edit your personal details.</p>
|
<p className="text-muted-foreground mt-2 text-sm">Here you can edit your personal details.</p>
|
||||||
|
|
||||||
<hr className="my-4" />
|
<hr className="my-4" />
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const PeriodSelector = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Select defaultValue={period} onValueChange={onPeriodChange}>
|
<Select defaultValue={period} onValueChange={onPeriodChange}>
|
||||||
<SelectTrigger className="max-w-[200px] text-slate-500">
|
<SelectTrigger className="text-muted-foreground max-w-[200px]">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
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 { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||||
import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
|
import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
|
||||||
|
|
||||||
@@ -8,12 +9,20 @@ export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
|
|||||||
documentId: number;
|
documentId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const completeDocument = async ({ documentId }: CompleteDocumentActionInput) => {
|
export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => {
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
const { id: userId } = await getRequiredServerComponentSession();
|
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,
|
userId,
|
||||||
documentId,
|
documentId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Loader } from 'lucide-react';
|
import { Eye, EyeOff, Loader } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -36,6 +38,9 @@ export type PasswordFormProps = {
|
|||||||
export const PasswordForm = ({ className }: PasswordFormProps) => {
|
export const PasswordForm = ({ className }: PasswordFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -92,15 +97,31 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
Password
|
Password
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Input
|
<div className="relative">
|
||||||
id="password"
|
<Input
|
||||||
type="password"
|
id="password"
|
||||||
minLength={6}
|
type={showPassword ? 'text' : 'password'}
|
||||||
maxLength={72}
|
minLength={6}
|
||||||
autoComplete="new-password"
|
maxLength={72}
|
||||||
className="bg-background mt-2"
|
autoComplete="new-password"
|
||||||
{...register('password')}
|
className="bg-background mt-2 pr-10"
|
||||||
/>
|
{...register('password')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
type="button"
|
||||||
|
className="absolute right-0 top-0 flex h-full items-center justify-center pr-3"
|
||||||
|
aria-label={showPassword ? 'Mask password' : 'Reveal password'}
|
||||||
|
onClick={() => setShowPassword((show) => !show)}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="text-muted-foreground h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Eye className="text-muted-foreground h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormErrorMessage className="mt-1.5" error={errors.password} />
|
<FormErrorMessage className="mt-1.5" error={errors.password} />
|
||||||
</div>
|
</div>
|
||||||
@@ -110,15 +131,31 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
Repeat Password
|
Repeat Password
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Input
|
<div className="relative">
|
||||||
id="repeated-password"
|
<Input
|
||||||
type="password"
|
id="repeated-password"
|
||||||
minLength={6}
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
maxLength={72}
|
minLength={6}
|
||||||
autoComplete="new-password"
|
maxLength={72}
|
||||||
className="bg-background mt-2"
|
autoComplete="new-password"
|
||||||
{...register('repeatedPassword')}
|
className="bg-background mt-2 pr-10"
|
||||||
/>
|
{...register('repeatedPassword')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
type="button"
|
||||||
|
className="absolute right-0 top-0 flex h-full items-center justify-center pr-3"
|
||||||
|
aria-label={showConfirmPassword ? 'Mask password' : 'Reveal password'}
|
||||||
|
onClick={() => setShowConfirmPassword((show) => !show)}
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? (
|
||||||
|
<EyeOff className="text-muted-foreground h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Eye className="text-muted-foreground h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormErrorMessage className="mt-1.5" error={errors.repeatedPassword} />
|
<FormErrorMessage className="mt-1.5" error={errors.repeatedPassword} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Eye, EyeOff } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -37,6 +40,9 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
|
|||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
reset,
|
reset,
|
||||||
@@ -96,15 +102,31 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
|
|||||||
<span>Password</span>
|
<span>Password</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Input
|
<div className="relative">
|
||||||
id="password"
|
<Input
|
||||||
type="password"
|
id="password"
|
||||||
minLength={6}
|
type={showPassword ? 'text' : 'password'}
|
||||||
maxLength={72}
|
minLength={6}
|
||||||
autoComplete="current-password"
|
maxLength={72}
|
||||||
className="bg-background mt-2"
|
autoComplete="new-password"
|
||||||
{...register('password')}
|
className="bg-background mt-2 pr-10"
|
||||||
/>
|
{...register('password')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
type="button"
|
||||||
|
className="absolute right-0 top-0 flex h-full items-center justify-center pr-3"
|
||||||
|
aria-label={showPassword ? 'Mask password' : 'Reveal password'}
|
||||||
|
onClick={() => setShowPassword((show) => !show)}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="text-muted-foreground h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Eye className="text-muted-foreground h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormErrorMessage className="mt-1.5" error={errors.password} />
|
<FormErrorMessage className="mt-1.5" error={errors.password} />
|
||||||
</div>
|
</div>
|
||||||
@@ -114,15 +136,31 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
|
|||||||
<span>Repeat Password</span>
|
<span>Repeat Password</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Input
|
<div className="relative">
|
||||||
id="repeatedPassword"
|
<Input
|
||||||
type="password"
|
id="repeated-password"
|
||||||
minLength={6}
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
maxLength={72}
|
minLength={6}
|
||||||
autoComplete="current-password"
|
maxLength={72}
|
||||||
className="bg-background mt-2"
|
autoComplete="new-password"
|
||||||
{...register('repeatedPassword')}
|
className="bg-background mt-2 pr-10"
|
||||||
/>
|
{...register('repeatedPassword')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
type="button"
|
||||||
|
className="absolute right-0 top-0 flex h-full items-center justify-center pr-3"
|
||||||
|
aria-label={showConfirmPassword ? 'Mask password' : 'Reveal password'}
|
||||||
|
onClick={() => setShowConfirmPassword((show) => !show)}
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? (
|
||||||
|
<EyeOff className="text-muted-foreground h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Eye className="text-muted-foreground h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormErrorMessage className="mt-1.5" error={errors.repeatedPassword} />
|
<FormErrorMessage className="mt-1.5" error={errors.repeatedPassword} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Loader } from 'lucide-react';
|
import { Eye, EyeOff, Loader } from 'lucide-react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FcGoogle } from 'react-icons/fc';
|
import { FcGoogle } from 'react-icons/fc';
|
||||||
@@ -43,6 +43,7 @@ export const SignInForm = ({ className }: SignInFormProps) => {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -128,15 +129,31 @@ export const SignInForm = ({ className }: SignInFormProps) => {
|
|||||||
<span>Password</span>
|
<span>Password</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Input
|
<div className="relative">
|
||||||
id="password"
|
<Input
|
||||||
type="password"
|
id="password"
|
||||||
minLength={6}
|
type={showPassword ? 'text' : 'password'}
|
||||||
maxLength={72}
|
minLength={6}
|
||||||
autoComplete="current-password"
|
maxLength={72}
|
||||||
className="bg-background mt-2"
|
autoComplete="current-password"
|
||||||
{...register('password')}
|
className="bg-background mt-2 pr-10"
|
||||||
/>
|
{...register('password')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
type="button"
|
||||||
|
className="absolute right-0 top-0 flex h-full items-center justify-center pr-3"
|
||||||
|
aria-label={showPassword ? 'Mask password' : 'Reveal password'}
|
||||||
|
onClick={() => setShowPassword((show) => !show)}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="text-muted-foreground h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Eye className="text-muted-foreground h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormErrorMessage className="mt-1.5" error={errors.password} />
|
<FormErrorMessage className="mt-1.5" error={errors.password} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Loader } from 'lucide-react';
|
import { Eye, EyeOff, Loader } from 'lucide-react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -31,6 +33,7 @@ export type SignUpFormProps = {
|
|||||||
|
|
||||||
export const SignUpForm = ({ className }: SignUpFormProps) => {
|
export const SignUpForm = ({ className }: SignUpFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@@ -106,15 +109,31 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
|
|||||||
Password
|
Password
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Input
|
<div className="relative">
|
||||||
id="password"
|
<Input
|
||||||
type="password"
|
id="password"
|
||||||
minLength={6}
|
type={showPassword ? 'text' : 'password'}
|
||||||
maxLength={72}
|
minLength={6}
|
||||||
autoComplete="new-password"
|
maxLength={72}
|
||||||
className="bg-background mt-2"
|
autoComplete="new-password"
|
||||||
{...register('password')}
|
className="bg-background mt-2 pr-10"
|
||||||
/>
|
{...register('password')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
type="button"
|
||||||
|
className="absolute right-0 top-0 flex h-full items-center justify-center pr-3"
|
||||||
|
aria-label={showPassword ? 'Mask password' : 'Reveal password'}
|
||||||
|
onClick={() => setShowPassword((show) => !show)}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="text-muted-foreground h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Eye className="text-muted-foreground h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -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';
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
@@ -29,11 +29,23 @@ export const TemplateDocumentCompleted = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Section className="flex-row items-center justify-center">
|
<Section>
|
||||||
<div className="flex items-center justify-center p-4">
|
<Row className="table-fixed">
|
||||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
<Column />
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<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]">
|
<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" />
|
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
||||||
Completed
|
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';
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
@@ -30,13 +30,26 @@ export const TemplateDocumentInvite = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Section className="mt-4 flex-row items-center justify-center">
|
<Section className="mt-4">
|
||||||
<div className="flex items-center justify-center p-4">
|
<Row className="table-fixed">
|
||||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
<Column />
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<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">
|
<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>
|
||||||
|
|
||||||
<Text className="my-1 text-center text-base text-slate-400">
|
<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';
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
@@ -25,11 +25,23 @@ export const TemplateDocumentPending = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Section className="flex-row items-center justify-center">
|
<Section>
|
||||||
<div className="flex items-center justify-center p-4">
|
<Row className="table-fixed">
|
||||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
<Column />
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<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">
|
<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" />
|
<Img src={getAssetUrl('/static/clock.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
||||||
Waiting for others
|
Waiting for others
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ import {
|
|||||||
} from '../template-components/template-document-invite';
|
} from '../template-components/template-document-invite';
|
||||||
import TemplateFooter from '../template-components/template-footer';
|
import TemplateFooter from '../template-components/template-footer';
|
||||||
|
|
||||||
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps>;
|
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
|
||||||
|
customBody?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const DocumentInviteEmailTemplate = ({
|
export const DocumentInviteEmailTemplate = ({
|
||||||
inviterName = 'Lucas Smith',
|
inviterName = 'Lucas Smith',
|
||||||
@@ -28,6 +30,7 @@ export const DocumentInviteEmailTemplate = ({
|
|||||||
documentName = 'Open Source Pledge.pdf',
|
documentName = 'Open Source Pledge.pdf',
|
||||||
signDocumentLink = 'https://documenso.com',
|
signDocumentLink = 'https://documenso.com',
|
||||||
assetBaseUrl = 'http://localhost:3002',
|
assetBaseUrl = 'http://localhost:3002',
|
||||||
|
customBody,
|
||||||
}: DocumentInviteEmailTemplateProps) => {
|
}: DocumentInviteEmailTemplateProps) => {
|
||||||
const previewText = `Completed Document`;
|
const previewText = `Completed Document`;
|
||||||
|
|
||||||
@@ -78,7 +81,11 @@ export const DocumentInviteEmailTemplate = ({
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text className="mt-2 text-base text-slate-400">
|
<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>
|
</Text>
|
||||||
</Section>
|
</Section>
|
||||||
</Container>
|
</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: {
|
include: {
|
||||||
documentData: true,
|
documentData: true,
|
||||||
|
documentMeta: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { createElement } from 'react';
|
|||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import { render } from '@documenso/email/render';
|
import { render } from '@documenso/email/render';
|
||||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
||||||
|
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export interface SendDocumentOptions {
|
export type SendDocumentOptions = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) => {
|
export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) => {
|
||||||
const user = await prisma.user.findFirstOrThrow({
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
@@ -25,9 +26,12 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
Recipient: true,
|
Recipient: true,
|
||||||
|
documentMeta: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const customEmail = document?.documentMeta;
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
throw new Error('Document not found');
|
throw new Error('Document not found');
|
||||||
}
|
}
|
||||||
@@ -44,6 +48,12 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
document.Recipient.map(async (recipient) => {
|
document.Recipient.map(async (recipient) => {
|
||||||
const { email, name } = recipient;
|
const { email, name } = recipient;
|
||||||
|
|
||||||
|
const customEmailTemplate = {
|
||||||
|
'signer.name': name,
|
||||||
|
'signer.email': email,
|
||||||
|
'document.name': document.title,
|
||||||
|
};
|
||||||
|
|
||||||
if (recipient.sendStatus === SendStatus.SENT) {
|
if (recipient.sendStatus === SendStatus.SENT) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -57,6 +67,7 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
inviterEmail: user.email,
|
inviterEmail: user.email,
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
signDocumentLink,
|
signDocumentLink,
|
||||||
|
customBody: renderCustomEmailTemplate(customEmail?.message || '', customEmailTemplate),
|
||||||
});
|
});
|
||||||
|
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
@@ -68,7 +79,9 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
|||||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
||||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
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),
|
html: render(template),
|
||||||
text: render(template, { plainText: true }),
|
text: render(template, { plainText: true }),
|
||||||
});
|
});
|
||||||
|
|||||||
21
packages/lib/server-only/document/update-document.ts
Normal file
21
packages/lib/server-only/document/update-document.ts
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
12
packages/lib/utils/render-custom-email-template.ts
Normal file
12
packages/lib/utils/render-custom-email-template.ts
Normal file
@@ -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;
|
||||||
@@ -110,6 +110,7 @@ model Document {
|
|||||||
Field Field[]
|
Field Field[]
|
||||||
documentDataId String
|
documentDataId String
|
||||||
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
||||||
|
documentMeta DocumentMeta?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
@@ -130,6 +131,14 @@ model DocumentData {
|
|||||||
Document Document?
|
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 {
|
enum ReadStatus {
|
||||||
NOT_OPENED
|
NOT_OPENED
|
||||||
OPENED
|
OPENED
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Document, DocumentData } from '@documenso/prisma/client';
|
import { Document, DocumentData, DocumentMeta } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export type DocumentWithData = Document & {
|
export type DocumentWithData = Document & {
|
||||||
documentData?: DocumentData | null;
|
documentData?: DocumentData | null;
|
||||||
|
documentMeta?: DocumentMeta | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
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 { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
@@ -21,7 +22,7 @@ export type AddSubjectFormProps = {
|
|||||||
documentFlow: DocumentFlowStep;
|
documentFlow: DocumentFlowStep;
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
document: Document;
|
document: DocumentWithData;
|
||||||
numberOfSteps: number;
|
numberOfSteps: number;
|
||||||
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
||||||
};
|
};
|
||||||
@@ -41,8 +42,8 @@ export const AddSubjectFormPartial = ({
|
|||||||
} = useForm<TAddSubjectFormSchema>({
|
} = useForm<TAddSubjectFormSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: {
|
email: {
|
||||||
subject: '',
|
subject: document.documentMeta?.subject ?? '',
|
||||||
message: '',
|
message: document.documentMeta?.message ?? '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user