Merge branch 'main' into chore/optimise-deps

This commit is contained in:
Lucas Smith
2023-05-01 20:36:37 +10:00
committed by GitHub
22 changed files with 247 additions and 107 deletions
+10 -10
View File
@@ -78,7 +78,7 @@ The current project goal is to <b>[release a production ready version](https://g
Documenso is built using awesome open source tech including: Documenso is built using awesome open source tech including:
- [Typescript](https://www.typescriptlang.org/) - [Typescript](https://www.typescriptlang.org/)
- [Javascript (when neccessary)](https://developer.mozilla.org/en-US/docs/Web/JavaScript) - [Javascript (when necessary)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [NextJS (JS Fullstack Framework)](https://nextjs.org/) - [NextJS (JS Fullstack Framework)](https://nextjs.org/)
- [Postgres SQL (Database)](https://www.postgresql.org/) - [Postgres SQL (Database)](https://www.postgresql.org/)
- [Prisma (ORM - Object-relational mapping)](https://www.prisma.io/) - [Prisma (ORM - Object-relational mapping)](https://www.prisma.io/)
@@ -96,7 +96,7 @@ Documenso is built using awesome open source tech including:
To run Documenso locally you need To run Documenso locally you need
- [Node.js (Version: >=18.x)](https://nodejs.org/en/download/) - [Node.js (Version: >=18.x)](https://nodejs.org/en/download/)
- Node Package Manger NPM - included in Node.js - Node Package Manager NPM - included in Node.js
- [PostgreSQL (local or remote)](https://www.postgresql.org/download/) - [PostgreSQL (local or remote)](https://www.postgresql.org/download/)
## Developer Quickstart ## Developer Quickstart
@@ -128,7 +128,7 @@ Your database will also be available on port `5432`. You can connect to it using
## Developer Setup ## Developer Setup
Follow these steps to setup documenso on you local machnine: Follow these steps to setup documenso on you local machine:
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device. - [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
```sh ```sh
@@ -138,12 +138,12 @@ Follow these steps to setup documenso on you local machnine:
- Rename <code>.env.example</code> to <code>.env</code> - Rename <code>.env.example</code> to <code>.env</code>
- Set DATABASE_URL value in .env file - Set DATABASE_URL value in .env file
- You can use the provided test database url (may be wiped at any point) - You can use the provided test database url (may be wiped at any point)
- Or setup a local postgres sql instance (recommened) - Or setup a local postgres sql instance (recommended)
- Create the database scheme by running <code>db-migrate:dev</code> - Create the database scheme by running <code>db-migrate:dev</code>
- Setup your mail provider - Setup your mail provider
- Set <code>SENDGRID_API_KEY</code> value in .env file - Set <code>SENDGRID_API_KEY</code> value in .env file
- You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/). - You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/).
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the <code>SMTP\_\* varibles</code> in your .env - Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the <code>SMTP\_\* variables</code> in your .env
- Run <code>npm run dev</code> root directory to start - Run <code>npm run dev</code> root directory to start
- Register a new user at http://localhost:3000/signup - Register a new user at http://localhost:3000/signup
@@ -154,20 +154,20 @@ Follow these steps to setup documenso on you local machnine:
- Optional: Create your own signing certificate - Optional: Create your own signing certificate
- A demo certificate is provided in /app/web/ressources/certificate.p12 - A demo certificate is provided in /app/web/ressources/certificate.p12
- To generate you own using these steps and a linux Terminal or Windows Linux Subsystem see **Create your own signging certificate**. - To generate your own using these steps and a linux Terminal or Windows Linux Subsystem see **Create your own signing certificate**.
## Updating ## Updating
- If you pull the newest version from main, using <code>git pull</code>, it may be neccessary to regenerate your database client - If you pull the newest version from main, using <code>git pull</code>, it may be necessary to regenerate your database client
- You can do this by running the generate command in /packages/prisma: - You can do this by running the generate command in /packages/prisma:
```sh ```sh
npx prisma generate npx prisma generate
``` ```
- This is not neccessary on first clone - This is not necessary on first clone
# Creating your own signging certificate # Creating your own signing certificate
For the digital signature of you documents you need a signign certificate in .p12 formate (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one: For the digital signature of your documents you need a signing certificate in .p12 formate (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:\ 1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:\
<code>openssl genrsa -out private.key 2048</code> <code>openssl genrsa -out private.key 2048</code>
+1 -2
View File
@@ -80,12 +80,11 @@ export default function PDFSigner(props: any) {
: props.document.User.email}{" "} : props.document.User.email}{" "}
would like you to sign this document. would like you to sign this document.
</p> </p>
<p className="mt-3 text-sm md:mt-0 md:ml-6"> <p className="mt-3 text-sm md:mt-0 md:ml-6 text-right md:text-inherit">
<Button <Button
disabled={!signingDone} disabled={!signingDone}
color="secondary" color="secondary"
icon={CheckBadgeIcon} icon={CheckBadgeIcon}
className="float-right"
onClick={() => { onClick={() => {
signDocument(props.document, localSignatures, `${router.query.token}`).then( signDocument(props.document, localSignatures, `${router.query.token}`).then(
() => { () => {
@@ -45,10 +45,11 @@ export default function RecipientSelector(props: any) {
{props?.recipients.map((recipient: any) => ( {props?.recipients.map((recipient: any) => (
<Listbox.Option <Listbox.Option
key={recipient?.id} key={recipient?.id}
disabled={!recipient?.email}
className={({ active }) => className={({ active }) =>
classNames( classNames(
active ? "bg-neon-dark text-white" : "text-gray-900", active ? "bg-neon-dark text-white" : "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9" "relative cursor-default select-none py-2 pl-3 pr-9 aria-disabled:opacity-50 aria-disabled:cursor-not-allowed"
) )
} }
value={recipient}> value={recipient}>
@@ -66,7 +67,7 @@ export default function RecipientSelector(props: any) {
selected ? "font-semibold" : "font-normal", selected ? "font-semibold" : "font-normal",
"ml-3 block truncate" "ml-3 block truncate"
)}> )}>
{`${recipient?.name} <${recipient?.email}>`} {`${recipient?.name} <${recipient?.email || 'unknown'}>`}
</span> </span>
</div> </div>
@@ -76,7 +77,7 @@ export default function RecipientSelector(props: any) {
active ? "text-white" : "text-neon-dark", active ? "text-white" : "text-neon-dark",
"absolute inset-y-0 right-0 flex items-center pr-4" "absolute inset-y-0 right-0 flex items-center pr-4"
)}> )}>
<CheckIcon className="h-5 w-5" aria-hidden="true" /> <CheckIcon className="h-5 w-5" strokeWidth={3} aria-hidden="true" />
</span> </span>
) : null} ) : null}
</> </>
+54 -42
View File
@@ -5,6 +5,7 @@ import { Button, IconButton } from "@documenso/ui";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { LanguageIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; import { LanguageIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
import SignatureCanvas from "react-signature-canvas"; import SignatureCanvas from "react-signature-canvas";
import { useDebouncedValue } from "../../hooks/use-debounced-value";
const tabs = [ const tabs = [
{ name: "Type", icon: LanguageIcon, current: true }, { name: "Type", icon: LanguageIcon, current: true },
@@ -15,6 +16,9 @@ export default function SignatureDialog(props: any) {
const [currentTab, setCurrentTab] = useState(tabs[0]); const [currentTab, setCurrentTab] = useState(tabs[0]);
const [typedSignature, setTypedSignature] = useState(""); const [typedSignature, setTypedSignature] = useState("");
const [signatureEmpty, setSignatureEmpty] = useState(true); const [signatureEmpty, setSignatureEmpty] = useState(true);
// This is a workaround to prevent the canvas from being rendered when the dialog is closed
// we also need the debounce to avoid rendering while transitions are occuring.
const showCanvas = useDebouncedValue<boolean>(props.open, 1);
let signCanvasRef: any | undefined; let signCanvasRef: any | undefined;
useEffect(() => { useEffect(() => {
@@ -85,7 +89,7 @@ export default function SignatureDialog(props: any) {
</div> </div>
{isCurrentTab("Type") ? ( {isCurrentTab("Type") ? (
<div> <div>
<div className="my-8 mb-3 border-b border-gray-300"> <div className="my-7 mb-3 border-b border-gray-300">
<input <input
value={typedSignature} value={typedSignature}
onChange={(e) => { onChange={(e) => {
@@ -98,7 +102,7 @@ export default function SignatureDialog(props: any) {
placeholder="Kindly type your name" placeholder="Kindly type your name"
/> />
</div> </div>
<div className="float-right"> <div className="flex flex-row-reverse items-center gap-x-3">
<Button <Button
color="secondary" color="secondary"
onClick={() => { onClick={() => {
@@ -126,47 +130,55 @@ export default function SignatureDialog(props: any) {
"" ""
)} )}
{isCurrentTab("Draw") ? ( {isCurrentTab("Draw") ? (
<div className=""> <div className="" key={props.open ? "closed" : "open"}>
<SignatureCanvas {showCanvas && (
ref={(ref) => { <SignatureCanvas
signCanvasRef = ref; ref={(ref) => {
}} signCanvasRef = ref;
canvasProps={{
className: "sigCanvas border-b b-2 border-slate w-full h-full mb-3",
}}
clearOnResize={true}
onEnd={() => {
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
/>
<IconButton
className="float-left block"
icon={TrashIcon}
onClick={() => {
signCanvasRef?.clear();
setSignatureEmpty(signCanvasRef?.isEmpty());
}}></IconButton>
<div className="float-right mt-10">
<Button
color="secondary"
onClick={() => {
props.onClose();
props.setOpen(false);
setCurrent(tabs[0]);
}}>
Cancel
</Button>
<Button
className="ml-3"
onClick={() => {
props.onClose({
type: "draw",
signatureImage: signCanvasRef.toDataURL("image/png"),
});
}} }}
disabled={signatureEmpty}> canvasProps={{
Sign className: "sigCanvas border-b b-2 border-slate w-full h-full mb-3",
</Button> }}
clearOnResize={true}
onEnd={() => {
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
/>
)}
<div className="flex items-center justify-between">
<IconButton
className="block"
icon={TrashIcon}
onClick={() => {
signCanvasRef?.clear();
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
/>
<div className="flex flex-row-reverse items-center gap-x-3">
<Button
color="secondary"
onClick={() => {
props.onClose();
props.setOpen(false);
setCurrent(tabs[0]);
}}>
Cancel
</Button>
<Button
className="ml-3"
onClick={() => {
props.onClose({
type: "draw",
signatureImage: signCanvasRef.toDataURL("image/png"),
});
}}
disabled={signatureEmpty}>
Sign
</Button>
</div>
</div> </div>
</div> </div>
) : ( ) : (
+3 -3
View File
@@ -111,7 +111,7 @@ export default function Login(props: any) {
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-sm"> <div className="text-sm">
<a href="#" className="text-neon hover:text-neon font-medium"> <a href="#" className="text-gray-500 hover:text-neon-700 font-medium">
Forgot your password? Forgot your password?
</a> </a>
</div> </div>
@@ -123,7 +123,7 @@ export default function Login(props: any) {
className="group relative flex w-full"> className="group relative flex w-full">
<span className="absolute inset-y-0 left-0 flex items-center pl-3"> <span className="absolute inset-y-0 left-0 flex items-center pl-3">
<LockClosedIcon <LockClosedIcon
className="text-neon-dark group-hover:text-neon h-5 w-5 disabled:disabled:bg-gray-600 disabled:group-hover:bg-gray-600" className="text-neon-700 group-hover:text-neon-dark-700 h-5 w-5 disabled:disabled:bg-gray-600 disabled:group-hover:bg-gray-600 duration-200"
aria-hidden="true" aria-hidden="true"
/> />
</span> </span>
@@ -141,7 +141,7 @@ export default function Login(props: any) {
{props.allowSignup ? ( {props.allowSignup ? (
<p className="mt-2 text-center text-sm text-gray-600"> <p className="mt-2 text-center text-sm text-gray-600">
Are you new here?{" "} Are you new here?{" "}
<Link href="/signup" className="text-neon hover:text-neon font-medium"> <Link href="/signup" className="text-gray-500 hover:text-neon-700 duration-200 font-medium">
Create a new Account Create a new Account
</Link> </Link>
</p> </p>
+1 -1
View File
@@ -187,7 +187,7 @@ export default function Signup(props: { source: string }) {
</div> </div>
<p className="mt-2 text-center text-sm text-gray-600"> <p className="mt-2 text-center text-sm text-gray-600">
Already have an account?{" "} Already have an account?{" "}
<Link href="/login" className="text-neon hover:text-neon font-medium"> <Link href="/login" className="text-gray-500 hover:text-neon-700 font-medium">
Sign In Sign In
</Link> </Link>
</p> </p>
+18
View File
@@ -0,0 +1,18 @@
import { useEffect, useState } from "react";
export function useDebouncedValue<T>(value: T, delay: number) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
+6 -1
View File
@@ -63,6 +63,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
}, },
data: { data: {
signingStatus: SigningStatus.SIGNED, signingStatus: SigningStatus.SIGNED,
signedAt: new Date(),
}, },
}); });
@@ -86,7 +87,11 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
where: { where: {
documentId: document.id, documentId: document.id,
type: { in: [FieldType.DATE, FieldType.TEXT] }, type: { in: [FieldType.DATE, FieldType.TEXT] },
recipientId: { in: signedRecipients.map((r) => r.id) },
}, },
include: {
Recipient: true,
}
}); });
// Insert fields other than signatures // Insert fields other than signatures
@@ -98,7 +103,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
month: "long", month: "long",
day: "numeric", day: "numeric",
year: "numeric", year: "numeric",
}).format(new Date()) }).format(field.Recipient?.signedAt ?? new Date())
: field.customText || "", : field.customText || "",
field.positionX, field.positionX,
field.positionY, field.positionY,
+12 -9
View File
@@ -1,4 +1,4 @@
import { ReactElement } from "react"; import { ChangeEvent, ReactElement } from "react";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { uploadDocument } from "@documenso/features"; import { uploadDocument } from "@documenso/features";
@@ -62,26 +62,27 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
<dl className="mt-8 grid gap-5 md:grid-cols-3 "> <dl className="mt-8 grid gap-5 md:grid-cols-3 ">
{stats.map((item) => ( {stats.map((item) => (
<Link href={item.link} key={item.name}> <Link href={item.link} key={item.name}>
<div className="overflow-hidden rounded-lg bg-white px-4 py-3 shadow sm:py-5 md:p-6"> <div className="overflow-hidden rounded-lg bg-white px-4 py-3 shadow hover:shadow-lg duration-300 sm:py-5 md:p-6">
<dt className="truncate text-sm font-medium text-gray-500 "> <dt className="truncate text-sm font-medium text-gray-700 ">
<item.icon <item.icon
className="text-neon mr-3 inline h-5 w-5 flex-shrink-0 sm:h-6 sm:w-6" className="text-neon-600 mr-3 inline h-5 w-5 flex-shrink-0 sm:h-6 sm:w-6"
aria-hidden="true"></item.icon> aria-hidden="true"></item.icon>
{item.name} {item.name}
</dt> </dt>
<dd className="mt-1 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl"> <dd className="mt-3 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
{getStat(item.name, props)} {getStat(item.name, props)}
</dd> </dd>
</div> </div>
</Link> </Link>
))} ))}
</dl> </dl>
<div className="mt-12"> <div className="mt-12">
<input <input
id="fileUploadHelper" id="fileUploadHelper"
type="file" type="file"
accept="application/pdf" accept="application/pdf"
onChange={(event: any) => { onChange={(event: ChangeEvent) => {
uploadDocument(event); uploadDocument(event);
}} }}
hidden hidden
@@ -91,9 +92,10 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
onClick={() => { onClick={() => {
document?.getElementById("fileUploadHelper")?.click(); document?.getElementById("fileUploadHelper")?.click();
}} }}
className="hover:border-neon relative block w-full cursor-pointer rounded-lg border-2 border-dashed border-gray-300 p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> className="group hover:border-neon-600 duration-200 relative block w-full cursor-pointer rounded-lg border-2 border-dashed border-gray-300 p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
<svg <svg
className="mx-auto h-12 w-12 text-gray-400" className="mx-auto h-12 w-12 text-gray-400 group-hover:text-gray-700 duration-200"
stroke="currentColor" stroke="currentColor"
fill="none" fill="none"
viewBox="0 00 20 25" viewBox="0 00 20 25"
@@ -104,7 +106,8 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
/> />
</svg> </svg>
<span id="add_document" className="text-neon mt-2 block text-sm font-medium">
<span id="add_document" className="text-gray-500 group-hover:text-neon-700 mt-2 block text-sm font-medium duration-200">
Add a new PDF document. Add a new PDF document.
</span> </span>
</div> </div>
+36 -16
View File
@@ -27,7 +27,13 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
const [filteredDocuments, setFilteredDocuments] = useState([]); const [filteredDocuments, setFilteredDocuments] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const statusFilters = [
type statusFilterType = {
label: string;
value: DocumentStatus | "ALL";
};
const statusFilters: statusFilterType[] = [
{ label: "All", value: "ALL" }, { label: "All", value: "ALL" },
{ label: "Draft", value: "DRAFT" }, { label: "Draft", value: "DRAFT" },
{ label: "Waiting for others", value: "PENDING" }, { label: "Waiting for others", value: "PENDING" },
@@ -83,6 +89,20 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
return filteredDocuments; return filteredDocuments;
} }
function handleStatusFilterChange(status: statusFilterType) {
router.replace(
{
pathname: router.pathname,
query: { filter: status.value },
},
undefined,
{
shallow: true, // Perform a shallow update, without reloading the page
}
);
setSelectedStatusFilter(status);
}
function wasXDaysAgoOrLess(documentDate: Date, lastXDays: number): boolean { function wasXDaysAgoOrLess(documentDate: Date, lastXDays: number): boolean {
if (lastXDays < 0) return true; if (lastXDays < 0) return true;
@@ -122,26 +142,26 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
</Button> </Button>
</div> </div>
</div> </div>
<div className="mt-3 mb-12"> <div className="mt-3 mb-12 flex flex-row-reverse items-center gap-x-4">
<div className="float-right ml-3 mt-7 block w-fit"> <div className="pt-5 block w-fit">
{filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"} {filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"}
</div> </div>
<SelectBox <SelectBox
className="float-right block w-1/4" className="block w-1/4"
label="Created" label="Created"
options={createdFilter} options={createdFilter}
value={selectedCreatedFilter} value={selectedCreatedFilter}
onChange={setSelectedCreatedFilter} onChange={setSelectedCreatedFilter}
/> />
<SelectBox <SelectBox
className="float-right ml-3 block w-1/4" className="block w-1/4"
label="Status" label="Status"
options={statusFilters} options={statusFilters}
value={selectedStatusFilter} value={selectedStatusFilter}
onChange={setSelectedStatusFilter} onChange={handleStatusFilterChange}
/> />
</div> </div>
<div className="mt-20 max-w-[1100px]" hidden={!loading}> <div className="mt-8 max-w-[1100px]" hidden={!loading}>
<div className="ph-item"> <div className="ph-item">
<div className="ph-col-12"> <div className="ph-col-12">
<div className="ph-picture"></div> <div className="ph-picture"></div>
@@ -158,7 +178,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
</div> </div>
</div> </div>
</div> </div>
<div className="mt-28 flex flex-col" hidden={!documents.length || loading}> <div className="mt-8 flex flex-col" hidden={!documents.length || loading}>
<div <div
className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8" className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8"
hidden={!documents.length || loading}> hidden={!documents.length || loading}>
@@ -201,13 +221,13 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{document.title || "#" + document.id} {document.title || "#" + document.id}
</td> </td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td className="whitespace-nowrap inline-flex py-3 gap-x-2 gap-y-1 flex-wrap max-w-[250px] text-sm text-gray-500">
{document.Recipient.map((item: any) => ( {document.Recipient.map((item: any) => (
<div key={item.id}> <div key={item.id}>
{item.sendStatus === "NOT_SENT" ? ( {item.sendStatus === "NOT_SENT" ? (
<span <span
id="sent_icon" id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"> className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
{item.name ? item.name + " <" + item.email + ">" : item.email} {item.name ? item.name + " <" + item.email + ">" : item.email}
</span> </span>
) : ( ) : (
@@ -217,8 +237,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<span id="sent_icon"> <span id="sent_icon">
<span <span
id="sent_icon" id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800"> className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
<EnvelopeIcon className="mr-1 inline h-5"></EnvelopeIcon> <EnvelopeIcon className="mr-1 inline h-4"></EnvelopeIcon>
{item.name ? item.name + " <" + item.email + ">" : item.email} {item.name ? item.name + " <" + item.email + ">" : item.email}
</span> </span>
</span> </span>
@@ -230,9 +250,9 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<span id="read_icon"> <span id="read_icon">
<span <span
id="sent_icon" id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800"> className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
<CheckIcon className="-mr-2 inline h-5"></CheckIcon> <CheckIcon className="-mr-2 inline h-4"></CheckIcon>
<CheckIcon className="mr-1 inline h-5"></CheckIcon> <CheckIcon className="mr-1 inline h-4"></CheckIcon>
{item.name ? item.name + " <" + item.email + ">" : item.email} {item.name ? item.name + " <" + item.email + ">" : item.email}
</span> </span>
</span> </span>
@@ -241,7 +261,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
)} )}
{item.signingStatus === "SIGNED" ? ( {item.signingStatus === "SIGNED" ? (
<span id="signed_icon"> <span id="signed_icon">
<span className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"> <span className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>{" "} <CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>{" "}
{item.email} {item.email}
</span> </span>
+11 -6
View File
@@ -18,13 +18,16 @@ import {
UserPlusIcon, UserPlusIcon,
XMarkIcon, XMarkIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { DocumentStatus, Document as PrismaDocument } from "@prisma/client"; import { DocumentStatus, Document as PrismaDocument, Recipient } from "@prisma/client";
import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form"; import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form";
import { toast } from "react-hot-toast";
export type FormValues = { export type FormValues = {
signers: { id: number; email: string; name: string }[]; signers: Array<Pick<Recipient, 'id' | 'email' | 'name' | 'sendStatus' | 'readStatus' | 'signingStatus'>>;
}; };
type FormSigner = FormValues["signers"][number];
const RecipientsPage: NextPageWithLayout = (props: any) => { const RecipientsPage: NextPageWithLayout = (props: any) => {
const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso"; const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso";
const breadcrumbItems = [ const breadcrumbItems = [
@@ -63,7 +66,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
}); });
const formValues = useWatch({ control, name: "signers" }); const formValues = useWatch({ control, name: "signers" });
const cancelButtonRef = useRef(null); const cancelButtonRef = useRef(null);
const hasEmailError = (formValue: any): boolean => { const hasEmailError = (formValue: FormSigner): boolean => {
const index = formValues.findIndex((e) => e.id === formValue.id); const index = formValues.findIndex((e) => e.id === formValue.id);
return !!errors?.signers?.[index]?.email; return !!errors?.signers?.[index]?.email;
}; };
@@ -108,12 +111,14 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
color="primary" color="primary"
icon={PaperAirplaneIcon} icon={PaperAirplaneIcon}
onClick={() => { onClick={() => {
setOpen(true); formValues.some((r) => r.email && hasEmailError(r))
? toast.error("Please enter a valid email address.", { id: "invalid email" })
: setOpen(true);
}} }}
disabled={ disabled={
(formValues.length || 0) === 0 || (formValues.length || 0) === 0 ||
!formValues.some( !formValues.some(
(r: any) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT" (r) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
) || ) ||
loading loading
}> }>
@@ -138,7 +143,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
trigger(); trigger();
}}> }}>
<ul role="list" className="divide-y divide-gray-200"> <ul role="list" className="divide-y divide-gray-200">
{fields.map((item: any, index: number) => ( {fields.map((item, index) => (
<li <li
key={index} key={index}
className="group w-full border-0 px-2 py-3 hover:bg-green-50 sm:py-4"> className="group w-full border-0 px-2 py-3 hover:bg-green-50 sm:py-4">
+11
View File
@@ -1,4 +1,5 @@
import Head from "next/head"; import Head from "next/head";
import { getUserFromToken } from "@documenso/lib/server";
import Login from "../components/login"; import Login from "../components/login";
export default function LoginPage(props: any) { export default function LoginPage(props: any) {
@@ -13,6 +14,16 @@ export default function LoginPage(props: any) {
} }
export async function getServerSideProps(context: any) { export async function getServerSideProps(context: any) {
const user = await getUserFromToken(context.req, context.res);
if (user)
return {
redirect: {
source: "/login",
destination: "/dashboard",
permanent: false,
},
};
const ALLOW_SIGNUP = process.env.ALLOW_SIGNUP === "true"; const ALLOW_SIGNUP = process.env.ALLOW_SIGNUP === "true";
return { return {
+11
View File
@@ -1,5 +1,6 @@
import { NextPageContext } from "next"; import { NextPageContext } from "next";
import Head from "next/head"; import Head from "next/head";
import { getUserFromToken } from "@documenso/lib/server";
import Signup from "../components/signup"; import Signup from "../components/signup";
export default function SignupPage(props: { source: string }) { export default function SignupPage(props: { source: string }) {
@@ -22,6 +23,16 @@ export async function getServerSideProps(context: any) {
}, },
}; };
const user = await getUserFromToken(context.req, context.res);
if (user)
return {
redirect: {
source: "/signup",
destination: "/dashboard",
permanent: false,
},
};
const signupSource: string = context.query["source"]; const signupSource: string = context.query["source"];
return { return {
props: { props: {
+42 -3
View File
@@ -16,9 +16,48 @@ module.exports = {
qwigley: ["Qwigley", "serif"], qwigley: ["Qwigley", "serif"],
}, },
colors: { colors: {
neon: "#37f095", neon: {
"neon-dark": "#2CC077", DEFAULT: "#37F095",
brown: "#353434", 50: "#E2FDF0",
100: "#CFFBE5",
200: "#A9F9D1",
300: "#83F6BD",
400: "#5DF3A9",
500: "#37F095",
600: "#11DE79",
700: "#0DAA5D",
800: "#097640",
900: "#054224",
950: "#032816",
},
"neon-dark": {
DEFAULT: "#2CC077",
50: "#B5EED2",
100: "#A5EAC8",
200: "#84E3B4",
300: "#62DBA0",
400: "#41D48B",
500: "#2CC077",
600: "#22925B",
700: "#17653E",
800: "#0D3722",
900: "#020906",
950: "#000000",
},
brown: {
DEFAULT: "#353434",
50: "#918F8F",
100: "#878585",
200: "#737171",
300: "#5E5C5C",
400: "#4A4848",
500: "#353434",
600: "#191818",
700: "#000000",
800: "#000000",
900: "#000000",
950: "#000000",
},
}, },
borderRadius: { borderRadius: {
"4xl": "2rem", "4xl": "2rem",
+3
View File
@@ -1,6 +1,8 @@
name: documenso
services: services:
database: database:
image: postgres:15 image: postgres:15
container_name: database
environment: environment:
- POSTGRES_USER=documenso - POSTGRES_USER=documenso
- POSTGRES_PASSWORD=password - POSTGRES_PASSWORD=password
@@ -10,6 +12,7 @@ services:
inbucket: inbucket:
image: inbucket/inbucket image: inbucket/inbucket
container_name: mailserver
ports: ports:
- 9000:9000 - 9000:9000
- 2500:2500 - 2500:2500
Submodule documenso updated: 0dcab27e65...8039871ab1
+6 -3
View File
@@ -1,9 +1,10 @@
import router from "next/router"; import router from "next/router";
import { NEXT_PUBLIC_WEBAPP_URL } from "../lib/constants"; import { NEXT_PUBLIC_WEBAPP_URL } from "../lib/constants";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { ChangeEvent } from "react";
export const uploadDocument = async (event: any) => { export const uploadDocument = async (event: ChangeEvent) => {
if (event.target.files && event.target.files[0]) { if (event.target instanceof HTMLInputElement && event.target?.files && event.target.files[0]) {
const body = new FormData(); const body = new FormData();
const document = event.target.files[0]; const document = event.target.files[0];
const fileName: string = event.target.files[0].name; const fileName: string = event.target.files[0].name;
@@ -12,8 +13,10 @@ export const uploadDocument = async (event: any) => {
toast.error("Non-PDF documents are not supported yet."); toast.error("Non-PDF documents are not supported yet.");
return; return;
} }
body.append("document", document || ""); body.append("document", document || "");
const response: any = await toast
await toast
.promise( .promise(
fetch("/api/documents", { fetch("/api/documents", {
method: "POST", method: "POST",
+8 -1
View File
@@ -1 +1,8 @@
export const NEXT_PUBLIC_WEBAPP_URL = process.env.NEXT_PUBLIC_WEBAPP_URL; export const NEXT_PUBLIC_WEBAPP_URL =
process.env.IS_PULL_REQUEST === "true"
? process.env.RENDER_EXTERNAL_URL
: process.env.NEXT_PUBLIC_WEBAPP_URL;
console.log("IS_PULL_REQUEST:" + process.env.IS_PULL_REQUEST);
console.log("RENDER_EXTERNAL_URL:" + process.env.RENDER_EXTERNAL_URL);
console.log("NEXT_PUBLIC_WEBAPP_URL:" + process.env.NEXT_PUBLIC_WEBAPP_URL);
+2 -2
View File
@@ -11,8 +11,8 @@ export const signingRequestTemplate = (
user: any user: any
) => { ) => {
const customContent = ` const customContent = `
<p style="margin: 30px;"> <p style="margin: 30px 0px; text-align: center">
<a href="${ctaLink}" style="background-color: #37f095; color: white; border-color: transparent; border-width: 1px; border-radius: 0.375rem; font-size: 18px; padding-left: 16px; padding-right: 16px; padding-top: 10px; padding-bottom: 10px; text-decoration: none; margin-top: 4px; margin-bottom: 4px;"> <a href="${ctaLink}" style="background-color: #37f095; white-space: nowrap; color: white; border-color: transparent; border-width: 1px; border-radius: 0.375rem; font-size: 18px; padding-left: 16px; padding-right: 16px; padding-top: 10px; padding-bottom: 10px; text-decoration: none; margin-top: 4px; margin-bottom: 4px;">
${ctaLabel} ${ctaLabel}
</a> </a>
</p> </p>
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Recipient" ADD COLUMN "signedAt" TIMESTAMP(3);
+1
View File
@@ -92,6 +92,7 @@ model Recipient {
name String @default("") @db.VarChar(255) name String @default("") @db.VarChar(255)
token String token String
expired DateTime? expired DateTime?
signedAt DateTime?
readStatus ReadStatus @default(NOT_OPENED) readStatus ReadStatus @default(NOT_OPENED)
signingStatus SigningStatus @default(NOT_SIGNED) signingStatus SigningStatus @default(NOT_SIGNED)
sendStatus SendStatus @default(NOT_SENT) sendStatus SendStatus @default(NOT_SENT)
+4 -4
View File
@@ -6,8 +6,8 @@ export function Button(props: any) {
const isLink = typeof props.href !== "undefined" && !props.disabled; const isLink = typeof props.href !== "undefined" && !props.disabled;
const { color = "primary", icon, disabled, onClick } = props; const { color = "primary", icon, disabled, onClick } = props;
const baseStyles = const baseStyles =
"inline-flex items-center justify-center min-w-[80px] rounded-md border border-transparent px-4 py-2 text-sm font-medium shadow-sm disabled:bg-gray-300"; "inline-flex gap-x-2 items-center justify-center min-w-[80px] rounded-md border border-transparent px-4 py-2 text-sm font-medium shadow-sm disabled:bg-gray-300 duration-200";
const primaryStyles = "text-white bg-neon hover:bg-neon-dark"; const primaryStyles = "text-gray-900 bg-neon hover:bg-neon-dark";
const secondaryStyles = "border-gray-300 bg-white text-gray-700 hover:bg-gray-50"; const secondaryStyles = "border-gray-300 bg-white text-gray-700 hover:bg-gray-50";
return isLink ? ( return isLink ? (
@@ -21,7 +21,7 @@ export function Button(props: any) {
)} )}
hidden={props.hidden}> hidden={props.hidden}>
{props.icon ? ( {props.icon ? (
<props.icon className="mr-1 inline h-5 text-inherit" aria-hidden="true"></props.icon> <props.icon className="inline-block h-5 text-inherit" aria-hidden="true"></props.icon>
) : ( ) : (
"" ""
)} )}
@@ -40,7 +40,7 @@ export function Button(props: any) {
disabled={props.disabled || props.loading} disabled={props.disabled || props.loading}
hidden={props.hidden}> hidden={props.hidden}>
{props.icon ? ( {props.icon ? (
<props.icon className="mr-1 inline h-5 text-inherit" aria-hidden="true"></props.icon> <props.icon className="inline h-5 text-inherit" aria-hidden="true"></props.icon>
) : ( ) : (
"" ""
)} )}