Merge remote-tracking branch 'origin/feat/refresh' into feat/single-player-mode

This commit is contained in:
Mythie
2023-09-24 22:18:01 +10:00
113 changed files with 2063 additions and 568 deletions

View File

@@ -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,
},
});
};

View File

@@ -4,6 +4,7 @@ import { prisma } from '@documenso/prisma';
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import { sealDocument } from './seal-document';
import { sendPendingEmail } from './send-pending-email';
export type CompleteDocumentWithTokenOptions = {
token: string;
@@ -69,6 +70,19 @@ export const completeDocumentWithToken = async ({
},
});
const pendingRecipients = await prisma.recipient.count({
where: {
documentId: document.id,
signingStatus: {
not: SigningStatus.SIGNED,
},
},
});
if (pendingRecipients > 0) {
await sendPendingEmail({ documentId, recipientId: recipient.id });
}
const documents = await prisma.document.updateMany({
where: {
id: document.id,

View File

@@ -13,6 +13,7 @@ export const getDocumentById = async ({ id, userId }: GetDocumentByIdOptions) =>
},
include: {
documentData: true,
documentMeta: true,
},
});
};

View File

@@ -9,6 +9,7 @@ import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import { getFile } from '../../universal/upload/get-file';
import { putFile } from '../../universal/upload/put-file';
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
import { sendCompletedEmail } from './send-completed-email';
export type SealDocumentOptions = {
documentId: number;
@@ -86,4 +87,6 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => {
data: newData,
},
});
await sendCompletedEmail({ documentId });
};

View File

@@ -0,0 +1,57 @@
import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
import { prisma } from '@documenso/prisma';
export interface SendDocumentOptions {
documentId: number;
}
export const sendCompletedEmail = async ({ documentId }: SendDocumentOptions) => {
const document = await prisma.document.findUnique({
where: {
id: documentId,
},
include: {
Recipient: true,
},
});
if (!document) {
throw new Error('Document not found');
}
if (document.Recipient.length === 0) {
throw new Error('Document has no recipients');
}
await Promise.all([
document.Recipient.map(async (recipient) => {
const { email, name, token } = recipient;
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const template = createElement(DocumentCompletedEmailTemplate, {
documentName: document.title,
assetBaseUrl,
downloadLink: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${token}/complete`,
});
await mailer.sendMail({
to: {
address: email,
name,
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Signing Complete!',
html: render(template),
text: render(template, { plainText: true }),
});
}),
]);
};

View File

@@ -4,13 +4,14 @@ import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
export interface SendDocumentOptions {
export type SendDocumentOptions = {
documentId: number;
userId: number;
}
};
export const sendDocument = async ({ documentId, userId }: SendDocumentOptions) => {
const user = await prisma.user.findFirstOrThrow({
@@ -26,9 +27,12 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
},
include: {
Recipient: true,
documentMeta: true,
},
});
const customEmail = document?.documentMeta;
if (!document) {
throw new Error('Document not found');
}
@@ -45,6 +49,12 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
document.Recipient.map(async (recipient) => {
const { email, name } = recipient;
const customEmailTemplate = {
'signer.name': name,
'signer.email': email,
'document.name': document.title,
};
if (recipient.sendStatus === SendStatus.SENT) {
return;
}
@@ -58,6 +68,7 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
inviterEmail: user.email,
assetBaseUrl,
signDocumentLink,
customBody: renderCustomEmailTemplate(customEmail?.message || '', customEmailTemplate),
});
await mailer.sendMail({
@@ -69,7 +80,9 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: 'Please sign this document',
subject: customEmail?.subject
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
: 'Please sign this document',
html: render(template),
text: render(template, { plainText: true }),
});

View File

@@ -0,0 +1,64 @@
import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending';
import { prisma } from '@documenso/prisma';
export interface SendPendingEmailOptions {
documentId: number;
recipientId: number;
}
export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingEmailOptions) => {
const document = await prisma.document.findFirst({
where: {
id: documentId,
Recipient: {
some: {
id: recipientId,
},
},
},
include: {
Recipient: {
where: {
id: recipientId,
},
},
},
});
if (!document) {
throw new Error('Document not found');
}
if (document.Recipient.length === 0) {
throw new Error('Document has no recipients');
}
const [recipient] = document.Recipient;
const { email, name } = recipient;
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const template = createElement(DocumentPendingEmailTemplate, {
documentName: document.title,
assetBaseUrl,
});
await mailer.sendMail({
to: {
address: email,
name,
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Waiting for others to complete signing.',
html: render(template),
text: render(template, { plainText: true }),
});
};

View 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,
},
});
};

View File

@@ -13,6 +13,9 @@ export const getFieldsForDocument = async ({ documentId, userId }: GetFieldsForD
userId,
},
},
orderBy: {
id: 'asc',
},
});
return fields;

View File

@@ -1,11 +1,12 @@
import { prisma } from '@documenso/prisma';
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
import { FieldType, SendStatus, SigningStatus } from '@documenso/prisma/client';
export interface SetFieldsForDocumentOptions {
userId: number;
documentId: number;
fields: {
id?: number | null;
type: FieldType;
signerEmail: string;
pageNumber: number;
pageX: number;
@@ -54,62 +55,56 @@ export const setFieldsForDocument = async ({
return {
...field,
...existing,
_persisted: existing,
};
})
.filter((field) => {
return (
field.Recipient?.sendStatus !== SendStatus.SENT &&
field.Recipient?.signingStatus !== SigningStatus.SIGNED
field._persisted?.Recipient?.sendStatus !== SendStatus.SENT &&
field._persisted?.Recipient?.signingStatus !== SigningStatus.SIGNED
);
});
const persistedFields = await prisma.$transaction(
// Disabling as wrapping promises here causes type issues
// eslint-disable-next-line @typescript-eslint/promise-function-async
linkedFields.map((field) =>
field.id
? prisma.field.update({
where: {
id: field.id,
recipientId: field.recipientId,
documentId,
prisma.field.upsert({
where: {
id: field._persisted?.id ?? -1,
documentId,
},
update: {
page: field.pageNumber,
positionX: field.pageX,
positionY: field.pageY,
width: field.pageWidth,
height: field.pageHeight,
},
create: {
type: field.type,
page: field.pageNumber,
positionX: field.pageX,
positionY: field.pageY,
width: field.pageWidth,
height: field.pageHeight,
customText: '',
inserted: false,
Document: {
connect: {
id: documentId,
},
data: {
type: field.type,
page: field.pageNumber,
positionX: field.pageX,
positionY: field.pageY,
width: field.pageWidth,
height: field.pageHeight,
},
})
: prisma.field.create({
data: {
// TODO: Rewrite this entire transaction because this is a mess
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
type: field.type!,
page: field.pageNumber,
positionX: field.pageX,
positionY: field.pageY,
width: field.pageWidth,
height: field.pageHeight,
customText: '',
inserted: false,
Document: {
connect: {
id: document.id,
},
},
Recipient: {
connect: {
documentId_email: {
documentId: document.id,
email: field.signerEmail,
},
},
},
Recipient: {
connect: {
documentId_email: {
documentId,
email: field.signerEmail.toLowerCase(),
},
},
}),
},
},
}),
),
);

View File

@@ -16,6 +16,9 @@ export const getRecipientsForDocument = async ({
userId,
},
},
orderBy: {
id: 'asc',
},
});
return recipients;

View File

@@ -29,6 +29,11 @@ export const setRecipientsForDocument = async ({
throw new Error('Document not found');
}
const normalizedRecipients = recipients.map((recipient) => ({
...recipient,
email: recipient.email.toLowerCase(),
}));
const existingRecipients = await prisma.recipient.findMany({
where: {
documentId,
@@ -37,13 +42,13 @@ export const setRecipientsForDocument = async ({
const removedRecipients = existingRecipients.filter(
(existingRecipient) =>
!recipients.find(
!normalizedRecipients.find(
(recipient) =>
recipient.id === existingRecipient.id || recipient.email === existingRecipient.email,
),
);
const linkedRecipients = recipients
const linkedRecipients = normalizedRecipients
.map((recipient) => {
const existing = existingRecipients.find(
(existingRecipient) =>
@@ -62,27 +67,26 @@ export const setRecipientsForDocument = async ({
});
const persistedRecipients = await prisma.$transaction(
// Disabling as wrapping promises here causes type issues
// eslint-disable-next-line @typescript-eslint/promise-function-async
linkedRecipients.map((recipient) =>
recipient.id
? prisma.recipient.update({
where: {
id: recipient.id,
documentId,
},
data: {
name: recipient.name,
email: recipient.email,
documentId,
},
})
: prisma.recipient.create({
data: {
name: recipient.name,
email: recipient.email,
token: nanoid(),
documentId,
},
}),
prisma.recipient.upsert({
where: {
id: recipient.id ?? -1,
documentId,
},
update: {
name: recipient.name,
email: recipient.email,
documentId,
},
create: {
name: recipient.name,
email: recipient.email,
token: nanoid(),
documentId,
},
}),
),
);

View File

@@ -0,0 +1,58 @@
import { P, match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
import { alphaid } from '../../universal/id';
export type CreateSharingIdOptions =
| {
documentId: number;
token: string;
}
| {
documentId: number;
userId: number;
};
export const createOrGetShareLink = async ({ documentId, ...options }: CreateSharingIdOptions) => {
const email = await match(options)
.with({ token: P.string }, async ({ token }) => {
return await prisma.recipient
.findFirst({
where: {
documentId,
token,
},
})
.then((recipient) => recipient?.email);
})
.with({ userId: P.number }, async ({ userId }) => {
return await prisma.user
.findFirst({
where: {
id: userId,
},
})
.then((user) => user?.email);
})
.exhaustive();
if (!email) {
throw new Error('Unable to create share link for document with the given email');
}
return await prisma.documentShareLink.upsert({
where: {
documentId_email: {
email,
documentId,
},
},
create: {
email,
documentId,
slug: alphaid(14),
},
update: {},
});
};

View File

@@ -0,0 +1,42 @@
import { prisma } from '@documenso/prisma';
export type GetRecipientOrSenderByShareLinkSlugOptions = {
slug: string;
};
export const getRecipientOrSenderByShareLinkSlug = async ({
slug,
}: GetRecipientOrSenderByShareLinkSlugOptions) => {
const { documentId, email } = await prisma.documentShareLink.findFirstOrThrow({
where: {
slug,
},
});
const recipient = await prisma.recipient.findFirst({
where: {
documentId,
email,
},
include: {
Signature: true,
},
});
if (recipient) {
return recipient;
}
const sender = await prisma.user.findFirst({
where: {
Document: { some: { id: documentId } },
email,
},
});
if (sender) {
return sender;
}
throw new Error('Recipient or sender not found');
};

View File

@@ -0,0 +1,13 @@
import { prisma } from '@documenso/prisma';
export type GetShareLinkBySlugOptions = {
slug: string;
};
export const getShareLinkBySlug = async ({ slug }: GetShareLinkBySlugOptions) => {
return await prisma.documentShareLink.findFirstOrThrow({
where: {
slug,
},
});
};

View File

@@ -6,7 +6,7 @@ export type GetSubscriptionByUserIdOptions = {
userId: number;
};
export const getSubscriptionByUserId = ({ userId }: GetSubscriptionByUserIdOptions) => {
export const getSubscriptionByUserId = async ({ userId }: GetSubscriptionByUserIdOptions) => {
return prisma.subscription.findFirst({
where: {
userId,