mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
Merge remote-tracking branch 'origin/feat/refresh' into feat/single-player-mode
This commit is contained in:
@ -9,7 +9,9 @@
|
||||
"server-only/",
|
||||
"universal/"
|
||||
],
|
||||
"scripts": {},
|
||||
"scripts": {
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/lib": "*",
|
||||
"@documenso/prisma": "*"
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "email dev --port 3002 --dir templates",
|
||||
"clean": "rimraf node_modules",
|
||||
"worker:test": "tsup worker/index.ts --format esm"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -27,11 +27,23 @@ export const TemplateDocumentCompleted = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Section className="flex-row items-center justify-center">
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||
</div>
|
||||
<Section>
|
||||
<Row className="table-fixed">
|
||||
<Column />
|
||||
|
||||
<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]">
|
||||
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
||||
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';
|
||||
|
||||
@ -30,13 +30,26 @@ export const TemplateDocumentInvite = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Section className="mt-4 flex-row items-center justify-center">
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||
</div>
|
||||
<Section className="mt-4">
|
||||
<Row className="table-fixed">
|
||||
<Column />
|
||||
|
||||
<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">
|
||||
{inviterName} has invited you to sign "{documentName}"
|
||||
{inviterName} has invited you to sign
|
||||
<br />"{documentName}"
|
||||
</Text>
|
||||
|
||||
<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';
|
||||
|
||||
@ -25,11 +25,23 @@ export const TemplateDocumentPending = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Section className="flex-row items-center justify-center">
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||
</div>
|
||||
<Section>
|
||||
<Row className="table-fixed">
|
||||
<Column />
|
||||
|
||||
<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">
|
||||
<Img src={getAssetUrl('/static/clock.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
||||
Waiting for others
|
||||
|
||||
@ -20,7 +20,9 @@ import {
|
||||
} from '../template-components/template-document-invite';
|
||||
import TemplateFooter from '../template-components/template-footer';
|
||||
|
||||
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps>;
|
||||
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
|
||||
customBody?: string;
|
||||
};
|
||||
|
||||
export const DocumentInviteEmailTemplate = ({
|
||||
inviterName = 'Lucas Smith',
|
||||
@ -28,6 +30,7 @@ export const DocumentInviteEmailTemplate = ({
|
||||
documentName = 'Open Source Pledge.pdf',
|
||||
signDocumentLink = 'https://documenso.com',
|
||||
assetBaseUrl = 'http://localhost:3002',
|
||||
customBody,
|
||||
}: DocumentInviteEmailTemplateProps) => {
|
||||
const previewText = `Completed Document`;
|
||||
|
||||
@ -78,7 +81,11 @@ export const DocumentInviteEmailTemplate = ({
|
||||
</Text>
|
||||
|
||||
<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>
|
||||
</Section>
|
||||
</Container>
|
||||
|
||||
@ -3,6 +3,9 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.cjs",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||
"@typescript-eslint/parser": "^5.59.2",
|
||||
|
||||
@ -10,7 +10,9 @@
|
||||
"universal/",
|
||||
"next-auth/"
|
||||
],
|
||||
"scripts": {},
|
||||
"scripts": {
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.410.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.410.0",
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -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,
|
||||
|
||||
@ -13,6 +13,7 @@ export const getDocumentById = async ({ id, userId }: GetDocumentByIdOptions) =>
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -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 });
|
||||
};
|
||||
|
||||
57
packages/lib/server-only/document/send-completed-email.ts
Normal file
57
packages/lib/server-only/document/send-completed-email.ts
Normal 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 }),
|
||||
});
|
||||
}),
|
||||
]);
|
||||
};
|
||||
@ -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 }),
|
||||
});
|
||||
|
||||
64
packages/lib/server-only/document/send-pending-email.ts
Normal file
64
packages/lib/server-only/document/send-pending-email.ts
Normal 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 }),
|
||||
});
|
||||
};
|
||||
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,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -13,6 +13,9 @@ export const getFieldsForDocument = async ({ documentId, userId }: GetFieldsForD
|
||||
userId,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
return fields;
|
||||
|
||||
@ -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(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@ -16,6 +16,9 @@ export const getRecipientsForDocument = async ({
|
||||
userId,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
return recipients;
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
58
packages/lib/server-only/share/create-or-get-share-link.ts
Normal file
58
packages/lib/server-only/share/create-or-get-share-link.ts
Normal 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: {},
|
||||
});
|
||||
};
|
||||
@ -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');
|
||||
};
|
||||
13
packages/lib/server-only/share/get-share-link-by-slug.ts
Normal file
13
packages/lib/server-only/share/get-share-link-by-slug.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -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,
|
||||
|
||||
1
packages/lib/types/font.d.ts
vendored
Normal file
1
packages/lib/types/font.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module '*.ttf';
|
||||
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;
|
||||
});
|
||||
};
|
||||
@ -3,6 +3,9 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.cjs",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
||||
"prettier": "^2.8.8",
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Share" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"link" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"documentId" INTEGER,
|
||||
|
||||
CONSTRAINT "Share_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Share_link_key" ON "Share"("link");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Share" ADD CONSTRAINT "Share_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Share" ADD CONSTRAINT "Share_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@ -0,0 +1,16 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `userId` on the `Share` table. All the data in the column will be lost.
|
||||
- Added the required column `recipientId` to the `Share` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Share" DROP CONSTRAINT "Share_userId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Share" DROP COLUMN "userId",
|
||||
ADD COLUMN "recipientId" INTEGER NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Share" ADD CONSTRAINT "Share_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@ -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,35 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `Share` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Share" DROP CONSTRAINT "Share_documentId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Share" DROP CONSTRAINT "Share_recipientId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Share";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "DocumentShareLink" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"documentId" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "DocumentShareLink_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "DocumentShareLink_slug_key" ON "DocumentShareLink"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "DocumentShareLink_documentId_email_key" ON "DocumentShareLink"("documentId", "email");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "DocumentShareLink" ADD CONSTRAINT "DocumentShareLink_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE RESTRICT 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;
|
||||
@ -7,6 +7,7 @@
|
||||
"scripts": {
|
||||
"build": "prisma generate",
|
||||
"format": "prisma format",
|
||||
"clean": "rimraf node_modules",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate-dev": "prisma migrate dev",
|
||||
"prisma:migrate-deploy": "prisma migrate deploy",
|
||||
|
||||
@ -101,15 +101,17 @@ enum DocumentStatus {
|
||||
}
|
||||
|
||||
model Document {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
title String
|
||||
status DocumentStatus @default(DRAFT)
|
||||
status DocumentStatus @default(DRAFT)
|
||||
Recipient Recipient[]
|
||||
Field Field[]
|
||||
ShareLink DocumentShareLink[]
|
||||
documentDataId String
|
||||
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
||||
documentMeta DocumentMeta?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
@ -130,6 +132,14 @@ model DocumentData {
|
||||
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 {
|
||||
NOT_OPENED
|
||||
OPENED
|
||||
@ -200,3 +210,16 @@ model Signature {
|
||||
Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
|
||||
Field Field @relation(fields: [fieldId], references: [id], onDelete: Restrict)
|
||||
}
|
||||
|
||||
model DocumentShareLink {
|
||||
id Int @id @default(autoincrement())
|
||||
email String
|
||||
slug String @unique
|
||||
documentId Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
document Document @relation(fields: [documentId], references: [id])
|
||||
|
||||
@@unique([documentId, email])
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Document, DocumentData } from '@documenso/prisma/client';
|
||||
import { Document, DocumentData, DocumentMeta } from '@documenso/prisma/client';
|
||||
|
||||
export type DocumentWithData = Document & {
|
||||
documentData?: DocumentData | null;
|
||||
documentMeta?: DocumentMeta | null;
|
||||
};
|
||||
|
||||
@ -3,6 +3,9 @@
|
||||
"version": "0.0.0",
|
||||
"main": "index.cjs",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"autoprefixer": "^10.4.13",
|
||||
"postcss": "^8.4.21",
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
"types": "./index.ts",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/lib": "*",
|
||||
|
||||
@ -2,6 +2,7 @@ import { authRouter } from './auth-router/router';
|
||||
import { documentRouter } from './document-router/router';
|
||||
import { fieldRouter } from './field-router/router';
|
||||
import { profileRouter } from './profile-router/router';
|
||||
import { shareLinkRouter } from './share-link-router/router';
|
||||
import { procedure, router } from './trpc';
|
||||
|
||||
export const appRouter = router({
|
||||
@ -10,6 +11,7 @@ export const appRouter = router({
|
||||
profile: profileRouter,
|
||||
document: documentRouter,
|
||||
field: fieldRouter,
|
||||
shareLink: shareLinkRouter,
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
35
packages/trpc/server/share-link-router/router.ts
Normal file
35
packages/trpc/server/share-link-router/router.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { createOrGetShareLink } from '@documenso/lib/server-only/share/create-or-get-share-link';
|
||||
|
||||
import { procedure, router } from '../trpc';
|
||||
import { ZCreateOrGetShareLinkMutationSchema } from './schema';
|
||||
|
||||
export const shareLinkRouter = router({
|
||||
createOrGetShareLink: procedure
|
||||
.input(ZCreateOrGetShareLinkMutationSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
const { documentId, token } = input;
|
||||
|
||||
if (token) {
|
||||
return await createOrGetShareLink({ documentId, token });
|
||||
}
|
||||
|
||||
if (!ctx.user?.id) {
|
||||
throw new Error(
|
||||
'You must either provide a token or be logged in to create a sharing link.',
|
||||
);
|
||||
}
|
||||
|
||||
return await createOrGetShareLink({ documentId, userId: ctx.user.id });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to create a sharing link.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
10
packages/trpc/server/share-link-router/schema.ts
Normal file
10
packages/trpc/server/share-link-router/schema.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZCreateOrGetShareLinkMutationSchema = z.object({
|
||||
documentId: z.number(),
|
||||
token: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TCreateOrGetShareLinkMutationSchema = z.infer<
|
||||
typeof ZCreateOrGetShareLinkMutationSchema
|
||||
>;
|
||||
@ -3,6 +3,9 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"files": [
|
||||
"base.json",
|
||||
"nextjs.json",
|
||||
|
||||
@ -1,29 +1,34 @@
|
||||
import type { LucideIcon, LucideProps } from 'lucide-react/dist/lucide-react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
export const SignatureIcon = (({
|
||||
size = 24,
|
||||
color = 'currentColor',
|
||||
strokeWidth = 1.33,
|
||||
absoluteStrokeWidth,
|
||||
...props
|
||||
}: LucideProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M1.5 11H14.5M1.5 14C1.5 14 8.72 2 4.86938 2H4.875C2.01 2 1.97437 14.0694 8 6.51188V6.5C8 6.5 9 11.3631 11.5 7.52375V7.5C11.5 7.5 11.5 9 14.5 9"
|
||||
stroke={color}
|
||||
strokeWidth={absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}) as LucideIcon;
|
||||
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
||||
|
||||
export const SignatureIcon: LucideIcon = forwardRef(
|
||||
(
|
||||
{ size = 24, color = 'currentColor', strokeWidth = 1.33, absoluteStrokeWidth, ...props },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M1.5 11H14.5M1.5 14C1.5 14 8.72 2 4.86938 2H4.875C2.01 2 1.97437 14.0694 8 6.51188V6.5C8 6.5 9 11.3631 11.5 7.52375V7.5C11.5 7.5 11.5 9 14.5 9"
|
||||
stroke={color}
|
||||
strokeWidth={
|
||||
absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth
|
||||
}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SignatureIcon.displayName = 'SignatureIcon';
|
||||
|
||||
@ -12,7 +12,8 @@
|
||||
"index.tsx"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint \"**/*.ts*\""
|
||||
"lint": "eslint \"**/*.ts*\"",
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@documenso/tailwind-config": "*",
|
||||
@ -57,7 +58,7 @@
|
||||
"clsx": "^1.2.1",
|
||||
"cmdk": "^0.2.0",
|
||||
"framer-motion": "^10.12.8",
|
||||
"lucide-react": "^0.277.0",
|
||||
"lucide-react": "^0.279.0",
|
||||
"luxon": "^3.4.2",
|
||||
"next": "13.4.19",
|
||||
"pdfjs-dist": "3.6.172",
|
||||
|
||||
@ -292,7 +292,7 @@ export const AddFieldsFormPartial = ({
|
||||
{selectedField && (
|
||||
<Card
|
||||
className={cn(
|
||||
'pointer-events-none fixed z-50 cursor-pointer bg-white transition-opacity',
|
||||
'bg-background pointer-events-none fixed z-50 cursor-pointer transition-opacity',
|
||||
{
|
||||
'border-primary': isFieldWithinBounds,
|
||||
'opacity-50': !isFieldWithinBounds,
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
|
||||
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 { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
@ -21,7 +22,7 @@ export type AddSubjectFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
document: Document;
|
||||
document: DocumentWithData;
|
||||
numberOfSteps: number;
|
||||
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
||||
};
|
||||
@ -41,8 +42,8 @@ export const AddSubjectFormPartial = ({
|
||||
} = useForm<TAddSubjectFormSchema>({
|
||||
defaultValues: {
|
||||
email: {
|
||||
subject: '',
|
||||
message: '',
|
||||
subject: document.documentMeta?.subject ?? '',
|
||||
message: document.documentMeta?.message ?? '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -118,7 +118,7 @@ export const FieldItem = ({
|
||||
>
|
||||
{!disabled && (
|
||||
<button
|
||||
className="text-muted-foreground/50 hover:text-muted-foreground/80 absolute -right-2 -top-2 z-20 flex h-8 w-8 items-center justify-center rounded-full border bg-white shadow-[0_0_0_2px_theme(colors.gray.100/70%)]"
|
||||
className="text-muted-foreground/50 hover:text-muted-foreground/80 bg-background absolute -right-2 -top-2 z-20 flex h-8 w-8 items-center justify-center rounded-full border"
|
||||
onClick={() => onRemove?.()}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
@ -126,7 +126,7 @@ export const FieldItem = ({
|
||||
)}
|
||||
|
||||
<Card
|
||||
className={cn('h-full w-full bg-white', {
|
||||
className={cn('bg-background h-full w-full', {
|
||||
'border-primary': !disabled,
|
||||
'border-primary/80': active,
|
||||
})}
|
||||
|
||||
@ -41,34 +41,34 @@
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71% 4%;
|
||||
--foreground: 213 31% 91%;
|
||||
--background: 0 0% 14.9%;
|
||||
--foreground: 0 0% 97%;
|
||||
|
||||
--muted: 223 47% 11%;
|
||||
--muted-foreground: 215.4 16.3% 56.9%;
|
||||
--muted: 0 0% 23.4%;
|
||||
--muted-foreground: 0 0% 85%;
|
||||
|
||||
--popover: 224 71% 4%;
|
||||
--popover-foreground: 215 20.2% 65.1%;
|
||||
--popover: 0 0% 14.9%;
|
||||
--popover-foreground: 0 0% 90%;
|
||||
|
||||
--card: 224 71% 4%;
|
||||
--card-border: 216 34% 17%;
|
||||
--card: 0 0% 14.9%;
|
||||
--card-border: 0 0% 27.9%;
|
||||
--card-border-tint: 112 205 159;
|
||||
--card-foreground: 213 31% 91%;
|
||||
--card-foreground: 0 0% 95%;
|
||||
|
||||
--border: 216 34% 17%;
|
||||
--input: 216 34% 17%;
|
||||
--border: 0 0% 27.9%;
|
||||
--input: 0 0% 27.9%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 1.2%;
|
||||
--primary: 95.08 71.08% 67.45%;
|
||||
--primary-foreground: 95.08 71.08% 10%;
|
||||
|
||||
--secondary: 222.2 47.4% 11.2%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--secondary: 0 0% 23.4%;
|
||||
--secondary-foreground: 95.08 71.08% 67.45%;
|
||||
|
||||
--accent: 216 34% 17%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--accent: 0 0% 27.9%;
|
||||
--accent-foreground: 95.08 71.08% 67.45%;
|
||||
|
||||
--destructive: 0 63% 31%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--destructive: 0 87% 62%;
|
||||
--destructive-foreground: 0 87% 19%;
|
||||
|
||||
--ring: 95.08 71.08% 67.45%;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user