diff --git a/apps/web/components/editor/pdf-editor.tsx b/apps/web/components/editor/pdf-editor.tsx index ae28f4a7f..d3ab28bac 100644 --- a/apps/web/components/editor/pdf-editor.tsx +++ b/apps/web/components/editor/pdf-editor.tsx @@ -1,16 +1,12 @@ import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants"; import { useRouter } from "next/router"; import dynamic from "next/dynamic"; -import React, { Fragment, useState } from "react"; -import { Button } from "@documenso/ui"; -import short from "short-uuid"; +import { Fragment, useState } from "react"; import toast from "react-hot-toast"; import { FieldType } from "@prisma/client"; import { Listbox, RadioGroup, Transition } from "@headlessui/react"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/outline"; import { classNames } from "@documenso/lib"; -import Draggable from "react-draggable"; -import Logo from "../logo"; const stc = require("string-to-color"); const PDFViewer = dynamic(() => import("./pdf-viewer"), { diff --git a/apps/web/components/editor/pdf-signer.tsx b/apps/web/components/editor/pdf-signer.tsx index dda2b45b1..2ac329afc 100644 --- a/apps/web/components/editor/pdf-signer.tsx +++ b/apps/web/components/editor/pdf-signer.tsx @@ -7,6 +7,7 @@ import { useEffect, useState } from "react"; import { Button } from "@documenso/ui"; import { CheckBadgeIcon } from "@heroicons/react/24/outline"; import toast from "react-hot-toast"; +import { FieldType } from "@prisma/client"; const PDFViewer = dynamic(() => import("./pdf-viewer"), { ssr: false, @@ -83,6 +84,7 @@ export default function PDFSigner(props: any) { return ( <> + {JSON.stringify(signatures)}
@@ -114,6 +116,15 @@ export default function PDFSigner(props: any) { fields={fields} pdfUrl={`${NEXT_PUBLIC_WEBAPP_URL}/api/documents/${router.query.id}?token=${router.query.token}`} onClick={onClick} + onMouseDown={function onMouseDown(e: any, page: number) { + if ( + fields.filter((field) => field.type === FieldType.SIGNATURE) + .length === 0 + ) + createFieldForFreeSignature(e, page, props.recipient); + }} + onMouseUp={() => {}} + onDelete={onDeleteHandler} > ); @@ -127,4 +138,120 @@ export default function PDFSigner(props: any) { return signatures.length > 0; } } + + function createFieldForFreeSignature( + e: any, + page: number, + recipient: any + ): any { + var rect = e.target.getBoundingClientRect(); + var newFieldX = e.clientX - rect.left; //x position within the element. + var newFieldY = e.clientY - rect.top; //y position within the element. + const signatureField = { + id: -1, + page: page, + type: FieldType.FREE_SIGNATURE, + positionX: newFieldX.toFixed(0), + positionY: newFieldY.toFixed(0), + Recipient: recipient, + }; + + upsertField(props.document, signatureField).then((res) => { + setFields(fields.concat(res)); + setDialogField(res); + setOpen(true); + }); + + return signatureField; + } + + function onDeleteHandler(id: any) { + const field = fields.find((e) => e.id == id); + const fieldIndex = fields.map((item) => item.id).indexOf(id); + if (fieldIndex > -1) { + const fieldWithoutRemoved = [...fields]; + const removedField = fieldWithoutRemoved.splice(fieldIndex, 1); + setFields(fieldWithoutRemoved); + + const signaturesWithoutRemoved = [...signatures]; + const removedSignature = signaturesWithoutRemoved.splice( + signaturesWithoutRemoved.findIndex(function (i) { + return i.fieldId === id; + }), + 1 + ); + + setSignatures(signaturesWithoutRemoved); + deleteField(field).catch((err) => { + setFields(fieldWithoutRemoved.concat(removedField)); + setSignatures(signaturesWithoutRemoved.concat(removedSignature)); + }); + } + } + + async function deleteField(field: any) { + if (!field.id) { + return; + } + + try { + const deleted = toast.promise( + fetch("/api/documents/" + 0 + "/fields/" + field.id, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(field), + }).then((res) => { + if (!res.ok) { + throw new Error(res.status.toString()); + } + return res; + }), + { + loading: "Deleting...", + success: "Deleted.", + error: "Could not delete :/", + }, + { + id: "delete", + style: { + minWidth: "200px", + }, + } + ); + return deleted; + } catch (error) {} + } + + async function upsertField(document: any, field: any): Promise { + try { + const created = await toast.promise( + fetch("/api/documents/" + document.id + "/fields", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(field), + }).then((res) => { + if (!res.ok) { + throw new Error(res.status.toString()); + } + return res.json(); + }), + { + loading: "Saving...", + success: "Saved.", + error: "Could not save :/", + }, + { + id: "saving field", + style: { + minWidth: "200px", + }, + } + ); + return created; + } catch (error) {} + } } diff --git a/apps/web/components/editor/pdf-viewer.jsx b/apps/web/components/editor/pdf-viewer.jsx index e4d338b37..2d0808e43 100644 --- a/apps/web/components/editor/pdf-viewer.jsx +++ b/apps/web/components/editor/pdf-viewer.jsx @@ -78,6 +78,7 @@ export default function PDFViewer(props) { key={item.id} field={item} className="absolute" + onDelete={onDeleteHandler} > ) : ( { + e.preventDefault(); + e.stopPropagation(); + }} >
{ @@ -75,6 +80,8 @@ export default function ReadOnlyField(props: FieldPropsType) { const newField = { ...field }; newField.signature = null; setField(newField); + // remove not only signature but whole field if it is a freely places signature + if (field.type === "FREE_SIGNATURE") props.onDelete(field.id); }} />
diff --git a/apps/web/pages/documents/[id]/sign.tsx b/apps/web/pages/documents/[id]/sign.tsx index 0a62e1f7c..e38b99e22 100644 --- a/apps/web/pages/documents/[id]/sign.tsx +++ b/apps/web/pages/documents/[id]/sign.tsx @@ -3,10 +3,9 @@ import Head from "next/head"; import { NextPageWithLayout } from "../../_app"; import { ReadStatus } from "@prisma/client"; import PDFSigner from "../../../components/editor/pdf-signer"; -import Logo from "../../../components/logo"; import Link from "next/link"; -import { Button } from "@documenso/ui"; -import { CheckBadgeIcon, ClockIcon } from "@heroicons/react/24/outline"; +import { ClockIcon } from "@heroicons/react/24/outline"; +import { FieldType, DocumentStatus } from "@prisma/client"; const SignPage: NextPageWithLayout = (props: any) => { return ( @@ -15,7 +14,11 @@ const SignPage: NextPageWithLayout = (props: any) => { Sign | Documenso {!props.expired ? ( - + ) : ( <>
@@ -84,19 +87,8 @@ export async function getServerSideProps(context: any) { }, }); - const unsignedFields = await prisma.field.findMany({ - where: { - documentId: recipient.Document.id, - recipientId: recipient.id, - Signature: { is: null }, - }, - include: { - Recipient: true, - Signature: true, - }, - }); - - if (unsignedFields.length === 0) { + // Document was already signed + if (recipient.Document.status === DocumentStatus.COMPLETED) { return { redirect: { permanent: false, @@ -105,8 +97,31 @@ export async function getServerSideProps(context: any) { }; } + // Clean up unsigned free place fields from UI from previous page visits + // todo refactor free sign fields to be client side only + await prisma.field.deleteMany({ + where: { + type: { in: [FieldType.FREE_SIGNATURE] }, + Signature: { is: null }, + }, + }); + + const unsignedFields = await prisma.field.findMany({ + where: { + documentId: recipient.Document.id, + recipientId: recipient.id, + type: { in: [FieldType.SIGNATURE] }, + Signature: { is: null }, + }, + include: { + Recipient: true, + Signature: true, + }, + }); + return { props: { + recipient: JSON.parse(JSON.stringify(recipient)), document: JSON.parse(JSON.stringify(recipient.Document)), fields: JSON.parse(JSON.stringify(unsignedFields)), expired: recipient.expired diff --git a/apps/web/pages/documents/[id]/signed.tsx b/apps/web/pages/documents/[id]/signed.tsx index e64013764..ab1b2406b 100644 --- a/apps/web/pages/documents/[id]/signed.tsx +++ b/apps/web/pages/documents/[id]/signed.tsx @@ -102,7 +102,7 @@ export async function getServerSideProps(context: any) { return { props: { document: JSON.parse(JSON.stringify(recipient.Document)), - fields: fields, + fields: JSON.parse(JSON.stringify(fields)), }, }; } diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 1f450eaeb..085319584 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -102,6 +102,7 @@ model Recipient { enum FieldType { SIGNATURE + FREE_SIGNATURE DATE TEXT }