mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
Merge branch 'main' into fix-#41-db-migration-Signature_recipientId_fkey
This commit is contained in:
@ -66,7 +66,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"
|
||||||
)}>
|
)}>
|
||||||
{`${selectedRecipient?.name} <${selectedRecipient?.email}>`}
|
{`${recipient?.name} <${recipient?.email}>`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -73,6 +73,13 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const signedRecipients = await prisma.recipient.findMany({
|
||||||
|
where: {
|
||||||
|
documentId: recipient.documentId,
|
||||||
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Don't check for inserted, because currently no "sign again" scenarios exist and
|
// Don't check for inserted, because currently no "sign again" scenarios exist and
|
||||||
// this is probably the expected behaviour in unclean states.
|
// this is probably the expected behaviour in unclean states.
|
||||||
const nonSignatureFields = await prisma.field.findMany({
|
const nonSignatureFields = await prisma.field.findMany({
|
||||||
@ -126,7 +133,11 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.document = documentWithInserts;
|
document.document = documentWithInserts;
|
||||||
if (documentOwner) await sendSigningDoneMail(recipient, document, documentOwner);
|
if (documentOwner) await sendSigningDoneMail(document, documentOwner);
|
||||||
|
|
||||||
|
for (const signer of signedRecipients) {
|
||||||
|
await sendSigningDoneMail(document, signer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
|
|||||||
@ -291,6 +291,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
router.push("/documents/" + document.id);
|
router.push("/documents/" + document.id);
|
||||||
}}
|
}}
|
||||||
|
disabled={document.status === "COMPLETED"}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={ArrowDownTrayIcon}
|
icon={ArrowDownTrayIcon}
|
||||||
|
|||||||
@ -34,7 +34,10 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: props.document.title,
|
title: props.document.title,
|
||||||
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
|
href:
|
||||||
|
props.document.status !== DocumentStatus.COMPLETED
|
||||||
|
? NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id
|
||||||
|
: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Recipients",
|
title: "Recipients",
|
||||||
@ -88,37 +91,45 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
href={"/api/documents/" + props.document.id}>
|
href={"/api/documents/" + props.document.id}>
|
||||||
Download
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{props.document.status !== DocumentStatus.COMPLETED && (
|
||||||
icon={PencilSquareIcon}
|
<>
|
||||||
disabled={props.document.status === DocumentStatus.COMPLETED}
|
<Button
|
||||||
color={props.document.status === DocumentStatus.COMPLETED ? "primary" : "secondary"}
|
icon={PencilSquareIcon}
|
||||||
className="mr-2"
|
disabled={props.document.status === DocumentStatus.COMPLETED}
|
||||||
href={breadcrumbItems[1].href}>
|
color={
|
||||||
Edit Document
|
props.document.status === DocumentStatus.COMPLETED ? "primary" : "secondary"
|
||||||
</Button>
|
}
|
||||||
<Button
|
className="mr-2"
|
||||||
className="min-w-[125px]"
|
href={breadcrumbItems[1].href}>
|
||||||
color="primary"
|
Edit Document
|
||||||
icon={PaperAirplaneIcon}
|
</Button>
|
||||||
onClick={() => {
|
<Button
|
||||||
setOpen(true);
|
className="min-w-[125px]"
|
||||||
}}
|
color="primary"
|
||||||
disabled={
|
icon={PaperAirplaneIcon}
|
||||||
(formValues.length || 0) === 0 ||
|
onClick={() => {
|
||||||
!formValues.some(
|
setOpen(true);
|
||||||
(r: any) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
|
}}
|
||||||
) ||
|
disabled={
|
||||||
loading
|
(formValues.length || 0) === 0 ||
|
||||||
}>
|
!formValues.some(
|
||||||
Send
|
(r: any) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
|
||||||
</Button>
|
) ||
|
||||||
|
loading
|
||||||
|
}>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-10 overflow-hidden rounded-md bg-white p-4 shadow sm:p-6">
|
<div className="mt-10 overflow-hidden rounded-md bg-white p-4 shadow sm:p-6">
|
||||||
<div className="border-b border-gray-200 pb-3 sm:pb-5">
|
<div className="border-b border-gray-200 pb-3 sm:pb-5">
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900 ">Signers</h3>
|
<h3 className="text-lg font-medium leading-6 text-gray-900 ">Signers</h3>
|
||||||
<p className="mt-2 max-w-4xl text-sm text-gray-500">
|
<p className="mt-2 max-w-4xl text-sm text-gray-500">
|
||||||
The people who will sign the document.
|
{props.document.status !== DocumentStatus.COMPLETED
|
||||||
|
? "The people who will sign the document."
|
||||||
|
: "The people who signed the document."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
@ -215,9 +226,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800">
|
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800">
|
||||||
Not Sent
|
Not Sent
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : null}
|
||||||
""
|
|
||||||
)}
|
|
||||||
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
|
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
|
||||||
<span id="sent_icon">
|
<span id="sent_icon">
|
||||||
<span
|
<span
|
||||||
@ -226,9 +235,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
<CheckIcon className="mr-1 inline h-5" /> Sent
|
<CheckIcon className="mr-1 inline h-5" /> Sent
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : null}
|
||||||
""
|
|
||||||
)}
|
|
||||||
{item.readStatus === "OPENED" && item.signingStatus === "NOT_SIGNED" ? (
|
{item.readStatus === "OPENED" && item.signingStatus === "NOT_SIGNED" ? (
|
||||||
<span id="read_icon">
|
<span id="read_icon">
|
||||||
<span
|
<span
|
||||||
@ -239,77 +246,77 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
Seen
|
Seen
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : null}
|
||||||
""
|
|
||||||
)}
|
|
||||||
{item.signingStatus === "SIGNED" ? (
|
{item.signingStatus === "SIGNED" ? (
|
||||||
<span id="signed_icon">
|
<span id="signed_icon">
|
||||||
<span
|
<span
|
||||||
id="sent_icon"
|
id="sent_icon"
|
||||||
className="mt-3 inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
className="mt-3 inline-block flex-shrink-0 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>
|
||||||
Signed
|
Signed
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : null}
|
||||||
""
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-1 flex">
|
{props.document.status !== DocumentStatus.COMPLETED && (
|
||||||
<IconButton
|
<div className="mr-1 flex">
|
||||||
icon={PaperAirplaneIcon}
|
<IconButton
|
||||||
disabled={
|
icon={PaperAirplaneIcon}
|
||||||
!item.id ||
|
disabled={
|
||||||
item.sendStatus !== "SENT" ||
|
!item.id ||
|
||||||
item.signingStatus === "SIGNED" ||
|
item.sendStatus !== "SENT" ||
|
||||||
loading
|
item.signingStatus === "SIGNED" ||
|
||||||
}
|
loading
|
||||||
color="secondary"
|
|
||||||
className="my-auto mr-4 h-9"
|
|
||||||
onClick={() => {
|
|
||||||
if (confirm("Resend this signing request?")) {
|
|
||||||
setLoading(true);
|
|
||||||
sendSigningRequests(props.document, [item.id]).finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}>
|
color="secondary"
|
||||||
Resend
|
className="my-auto mr-4 h-9"
|
||||||
</IconButton>
|
onClick={() => {
|
||||||
<IconButton
|
if (confirm("Resend this signing request?")) {
|
||||||
icon={TrashIcon}
|
setLoading(true);
|
||||||
disabled={!item.id || item.sendStatus === "SENT" || loading}
|
sendSigningRequests(props.document, [item.id]).finally(() => {
|
||||||
onClick={() => {
|
setLoading(false);
|
||||||
const removedItem = { ...fields }[index];
|
});
|
||||||
remove(index);
|
}
|
||||||
deleteRecipient(item)?.catch((err) => {
|
}}>
|
||||||
append(removedItem);
|
Resend
|
||||||
});
|
</IconButton>
|
||||||
}}
|
<IconButton
|
||||||
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
|
icon={TrashIcon}
|
||||||
/>
|
disabled={!item.id || item.sendStatus === "SENT" || loading}
|
||||||
</div>
|
onClick={() => {
|
||||||
|
const removedItem = { ...fields }[index];
|
||||||
|
remove(index);
|
||||||
|
deleteRecipient(item)?.catch((err) => {
|
||||||
|
append(removedItem);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<Button
|
{props.document.status !== "COMPLETED" && (
|
||||||
icon={UserPlusIcon}
|
<Button
|
||||||
className="mt-3"
|
icon={UserPlusIcon}
|
||||||
onClick={() => {
|
className="mt-3"
|
||||||
createOrUpdateRecipient({
|
onClick={() => {
|
||||||
id: "",
|
createOrUpdateRecipient({
|
||||||
email: "",
|
id: "",
|
||||||
name: "",
|
email: "",
|
||||||
documentId: props.document.id,
|
name: "",
|
||||||
}).then((res) => {
|
documentId: props.document.id,
|
||||||
append(res);
|
}).then((res) => {
|
||||||
});
|
append(res);
|
||||||
}}>
|
});
|
||||||
Add Signer
|
}}>
|
||||||
</Button>
|
Add Signer
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
|
|||||||
import { sendMail } from "./sendMail";
|
import { sendMail } from "./sendMail";
|
||||||
import { Document as PrismaDocument } from "@prisma/client";
|
import { Document as PrismaDocument } from "@prisma/client";
|
||||||
|
|
||||||
export const sendSigningDoneMail = async (recipient: any, document: PrismaDocument, user: any) => {
|
export const sendSigningDoneMail = async (document: PrismaDocument, user: any) => {
|
||||||
await sendMail(
|
await sendMail(
|
||||||
user.email,
|
user.email,
|
||||||
`Completed: "${document.title}"`,
|
`Completed: "${document.title}"`,
|
||||||
|
|||||||
@ -12,27 +12,36 @@ export async function insertTextInPDF(
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const fontBytes = fs.readFileSync("public/fonts/Qwigley-Regular.ttf");
|
const fontBytes = fs.readFileSync("public/fonts/Qwigley-Regular.ttf");
|
||||||
|
|
||||||
const existingPdfBytes = pdfAsBase64;
|
const pdfDoc = await PDFDocument.load(pdfAsBase64);
|
||||||
|
|
||||||
const pdfDoc = await PDFDocument.load(existingPdfBytes);
|
|
||||||
pdfDoc.registerFontkit(fontkit);
|
pdfDoc.registerFontkit(fontkit);
|
||||||
const customFont = await pdfDoc.embedFont(fontBytes);
|
|
||||||
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
const font = await pdfDoc.embedFont(useHandwritingFont ? fontBytes : StandardFonts.Helvetica);
|
||||||
|
|
||||||
const pages = pdfDoc.getPages();
|
const pages = pdfDoc.getPages();
|
||||||
const pdfPage = pages[page];
|
const pdfPage = pages[page];
|
||||||
|
|
||||||
const textSize = useHandwritingFont ? 50 : 15;
|
const textSize = useHandwritingFont ? 50 : 15;
|
||||||
const textWidth = customFont.widthOfTextAtSize(text, textSize);
|
const textWidth = font.widthOfTextAtSize(text, textSize);
|
||||||
const textHeight = customFont.heightAtSize(textSize);
|
const textHeight = font.heightAtSize(textSize);
|
||||||
const fieldSize = { width: 192, height: 64 };
|
const fieldSize = { width: 192, height: 64 };
|
||||||
const invertedYPosition = pdfPage.getHeight() - positionY - fieldSize.height;
|
|
||||||
|
// Because pdf-lib use a bottom-left coordinate system, we need to invert the y position
|
||||||
|
// we then center the text in the middle by adding half the height of the text
|
||||||
|
// plus the height of the field and divide the result by 2
|
||||||
|
const invertedYPosition =
|
||||||
|
pdfPage.getHeight() - positionY - (fieldSize.height + textHeight / 2) / 2;
|
||||||
|
|
||||||
|
// We center the text by adding the width of the field, subtracting the width of the text
|
||||||
|
// and dividing the result by 2
|
||||||
|
const centeredXPosition = positionX + (fieldSize.width - textWidth) / 2;
|
||||||
|
|
||||||
pdfPage.drawText(text, {
|
pdfPage.drawText(text, {
|
||||||
x: positionX,
|
x: centeredXPosition,
|
||||||
y: invertedYPosition,
|
y: invertedYPosition,
|
||||||
size: textSize,
|
size: textSize,
|
||||||
font: useHandwritingFont ? customFont : helveticaFont,
|
|
||||||
color: rgb(0, 0, 0),
|
color: rgb(0, 0, 0),
|
||||||
|
font,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pdfAsUint8Array = await pdfDoc.save();
|
const pdfAsUint8Array = await pdfDoc.save();
|
||||||
|
|||||||
@ -24,7 +24,7 @@ async function createUser(userData: { email: string; password: string }) {
|
|||||||
async function main() {
|
async function main() {
|
||||||
console.info("Start seeding...");
|
console.info("Start seeding...");
|
||||||
const password = "123456789";
|
const password = "123456789";
|
||||||
const email = "example6@documenso.com";
|
const email = "example@documenso.com";
|
||||||
const user = await createUser({
|
const user = await createUser({
|
||||||
email: email,
|
email: email,
|
||||||
password: await hashPassword(password),
|
password: await hashPassword(password),
|
||||||
|
|||||||
Reference in New Issue
Block a user