Compare commits

..

1 Commits

Author SHA1 Message Date
31b6d41cd2 fix: improve sealing speed 2025-11-19 12:09:05 +11:00
4 changed files with 136 additions and 219 deletions

View File

@ -278,20 +278,8 @@ export const EnvelopeEditorFieldDragDrop = ({
onMouseDown={() => setSelectedField(field.type)}
data-selected={selectedField === field.type ? true : undefined}
className={cn(
'group flex h-12 cursor-pointer items-center justify-center rounded-lg border px-4 transition-colors',
'border-border group flex h-12 cursor-pointer items-center justify-center rounded-lg border px-4 transition-colors',
RECIPIENT_COLOR_STYLES[selectedRecipientColor].fieldButton,
{
'text-recipient-green border-recipient-green/30':
selectedRecipientColor === 'green',
'text-recipient-blue border-recipient-blue/30': selectedRecipientColor === 'blue',
'text-recipient-purple border-recipient-purple/30':
selectedRecipientColor === 'purple',
'text-recipient-orange border-recipient-orange/30':
selectedRecipientColor === 'orange',
'text-recipient-yellow border-recipient-yellow/30':
selectedRecipientColor === 'yellow',
'text-recipient-pink border-recipient-pink/30': selectedRecipientColor === 'pink',
},
)}
>
<p
@ -299,12 +287,12 @@ export const EnvelopeEditorFieldDragDrop = ({
'text-muted-foreground font-noto group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
field.className,
{
'text-recipient-green': selectedRecipientColor === 'green',
'text-recipient-blue': selectedRecipientColor === 'blue',
'text-recipient-purple': selectedRecipientColor === 'purple',
'text-recipient-orange': selectedRecipientColor === 'orange',
'text-recipient-yellow': selectedRecipientColor === 'yellow',
'text-recipient-pink': selectedRecipientColor === 'pink',
'group-hover:text-recipient-green': selectedRecipientColor === 'green',
'group-hover:text-recipient-blue': selectedRecipientColor === 'blue',
'group-hover:text-recipient-purple': selectedRecipientColor === 'purple',
'group-hover:text-recipient-orange': selectedRecipientColor === 'orange',
'group-hover:text-recipient-yellow': selectedRecipientColor === 'yellow',
'group-hover:text-recipient-pink': selectedRecipientColor === 'pink',
},
)}
>

View File

@ -106,13 +106,6 @@ export const EnvelopeEditorFieldsPage = () => {
editorFields.setSelectedRecipient(firstSelectableRecipient?.id ?? null);
}, []);
const canRecipientBeModified = useMemo(
() =>
editorFields.selectedRecipient !== null &&
canRecipientFieldsBeModified(editorFields.selectedRecipient, envelope.fields),
[editorFields.selectedRecipient, envelope.fields],
);
return (
<div className="relative flex h-full">
<div className="flex w-full flex-col overflow-y-auto">
@ -182,35 +175,32 @@ export const EnvelopeEditorFieldsPage = () => {
align="end"
/>
{!canRecipientBeModified && (
<Alert className="mt-4" variant="warning">
<AlertDescription>
<Trans>
This recipient can no longer be modified as they have signed a field, or
completed the document.
</Trans>
</AlertDescription>
</Alert>
)}
{editorFields.selectedRecipient &&
!canRecipientFieldsBeModified(editorFields.selectedRecipient, envelope.fields) && (
<Alert className="mt-4" variant="warning">
<AlertDescription>
<Trans>
This recipient can no longer be modified as they have signed a field, or
completed the document.
</Trans>
</AlertDescription>
</Alert>
)}
</section>
{canRecipientBeModified && (
<>
<Separator className="my-4" />
<Separator className="my-4" />
{/* Add fields section. */}
<section className="px-4">
<h3 className="text-foreground mb-2 text-sm font-semibold">
<Trans>Add Fields</Trans>
</h3>
{/* Add fields section. */}
<section className="px-4">
<h3 className="text-foreground mb-2 text-sm font-semibold">
<Trans>Add Fields</Trans>
</h3>
<EnvelopeEditorFieldDragDrop
selectedRecipientId={editorFields.selectedRecipient?.id ?? null}
selectedEnvelopeItemId={currentEnvelopeItem?.id ?? null}
/>
</section>
</>
)}
<EnvelopeEditorFieldDragDrop
selectedRecipientId={editorFields.selectedRecipient?.id ?? null}
selectedEnvelopeItemId={currentEnvelopeItem?.id ?? null}
/>
</section>
{/* Field details section. */}
<AnimateGenericFadeInOut key={editorFields.selectedField?.formId}>

View File

@ -25,7 +25,6 @@ import { signPdf } from '@documenso/signing';
import { AppError, AppErrorCode } from '../../../errors/app-error';
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
import { addRejectionStampToPdf } from '../../../server-only/pdf/add-rejection-stamp-to-pdf';
@ -62,171 +61,120 @@ export const run = async ({
}) => {
const { documentId, sendEmail = true, isResealing = false, requestMetadata } = payload;
const envelope = await prisma.envelope.findFirstOrThrow({
where: {
type: EnvelopeType.DOCUMENT,
secondaryId: mapDocumentIdToSecondaryId(documentId),
},
include: {
documentMeta: true,
recipients: true,
envelopeItems: {
include: {
documentData: true,
field: {
include: {
signature: true,
const { envelopeId, envelopeStatus, isRejected } = await io.runTask('seal-document', async () => {
const envelope = await prisma.envelope.findFirstOrThrow({
where: {
type: EnvelopeType.DOCUMENT,
secondaryId: mapDocumentIdToSecondaryId(documentId),
},
include: {
documentMeta: true,
recipients: true,
envelopeItems: {
include: {
documentData: true,
field: {
include: {
signature: true,
},
},
},
},
},
},
});
if (envelope.envelopeItems.length === 0) {
throw new Error('At least one envelope item required');
}
const settings = await getTeamSettings({
userId: envelope.userId,
teamId: envelope.teamId,
});
const isComplete =
envelope.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) ||
envelope.recipients.every((recipient) => recipient.signingStatus === SigningStatus.SIGNED);
if (!isComplete) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Document is not complete',
});
}
// Seems silly but we need to do this in case the job is re-ran
// after it has already run through the update task further below.
// eslint-disable-next-line @typescript-eslint/require-await
const documentStatus = await io.runTask('get-document-status', async () => {
return envelope.status;
});
if (envelope.envelopeItems.length === 0) {
throw new Error('At least one envelope item required');
}
// This is the same case as above.
let envelopeItems = await io.runTask(
'get-document-data-id',
// eslint-disable-next-line @typescript-eslint/require-await
async () => {
// eslint-disable-next-line unused-imports/no-unused-vars
return envelope.envelopeItems.map(({ field, ...rest }) => ({
...rest,
}));
},
);
const settings = await getTeamSettings({
userId: envelope.userId,
teamId: envelope.teamId,
});
if (envelopeItems.length < 1) {
throw new Error(`Document ${envelope.id} has no envelope items`);
}
const isComplete =
envelope.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) ||
envelope.recipients.every((recipient) => recipient.signingStatus === SigningStatus.SIGNED);
const recipients = await prisma.recipient.findMany({
where: {
envelopeId: envelope.id,
role: {
not: RecipientRole.CC,
},
},
});
if (!isComplete) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Document is not complete',
});
}
// Determine if the document has been rejected by checking if any recipient has rejected it
const rejectedRecipient = recipients.find(
(recipient) => recipient.signingStatus === SigningStatus.REJECTED,
);
let envelopeItems = envelope.envelopeItems;
const isRejected = Boolean(rejectedRecipient);
if (envelopeItems.length < 1) {
throw new Error(`Document ${envelope.id} has no envelope items`);
}
// Get the rejection reason from the rejected recipient
const rejectionReason = rejectedRecipient?.rejectionReason ?? '';
const fields = await prisma.field.findMany({
where: {
envelopeId: envelope.id,
},
include: {
signature: true,
},
});
// Skip the field check if the document is rejected
if (!isRejected && fieldsContainUnsignedRequiredField(fields)) {
throw new Error(`Document ${envelope.id} has unsigned required fields`);
}
if (isResealing) {
// If we're resealing we want to use the initial data for the document
// so we aren't placing fields on top of eachother.
envelopeItems = envelopeItems.map((envelopeItem) => ({
...envelopeItem,
documentData: {
...envelopeItem.documentData,
data: envelopeItem.documentData.initialData,
},
}));
}
if (!envelope.qrToken) {
await prisma.envelope.update({
const recipients = await prisma.recipient.findMany({
where: {
id: envelope.id,
},
data: {
qrToken: prefixedId('qr'),
envelopeId: envelope.id,
role: {
not: RecipientRole.CC,
},
},
});
}
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
// Determine if the document has been rejected by checking if any recipient has rejected it
const rejectedRecipient = recipients.find(
(recipient) => recipient.signingStatus === SigningStatus.REJECTED,
);
const { certificateData, auditLogData } = await getCertificateAndAuditLogData({
legacyDocumentId,
documentMeta: envelope.documentMeta,
settings,
});
const isRejected = Boolean(rejectedRecipient);
// !: The commented out code is our desired implementation but we're seemingly
// !: running into issues with inngest parallelism in production.
// !: Until this is resolved we will do this sequentially which is slower but
// !: will actually work.
// const decoratePromises: Array<Promise<{ oldDocumentDataId: string; newDocumentDataId: string }>> =
// [];
// Get the rejection reason from the rejected recipient
const rejectionReason = rejectedRecipient?.rejectionReason ?? '';
// for (const envelopeItem of envelopeItems) {
// const task = io.runTask(`decorate-${envelopeItem.id}`, async () => {
// const envelopeItemFields = envelope.envelopeItems.find(
// (item) => item.id === envelopeItem.id,
// )?.field;
const fields = await prisma.field.findMany({
where: {
envelopeId: envelope.id,
},
include: {
signature: true,
},
});
// if (!envelopeItemFields) {
// throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
// }
// Skip the field check if the document is rejected
if (!isRejected && fieldsContainUnsignedRequiredField(fields)) {
throw new Error(`Document ${envelope.id} has unsigned required fields`);
}
// return decorateAndSignPdf({
// envelope,
// envelopeItem,
// envelopeItemFields,
// isRejected,
// rejectionReason,
// certificateData,
// auditLogData,
// });
// });
if (isResealing) {
// If we're resealing we want to use the initial data for the document
// so we aren't placing fields on top of eachother.
envelopeItems = envelopeItems.map((envelopeItem) => ({
...envelopeItem,
documentData: {
...envelopeItem.documentData,
data: envelopeItem.documentData.initialData,
},
}));
}
// decoratePromises.push(task);
// }
if (!envelope.qrToken) {
await prisma.envelope.update({
where: {
id: envelope.id,
},
data: {
qrToken: prefixedId('qr'),
},
});
}
// const newDocumentData = await Promise.all(decoratePromises);
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
// TODO: Remove once parallelization is working
const newDocumentData: Array<{ oldDocumentDataId: string; newDocumentDataId: string }> = [];
const { certificateData, auditLogData } = await getCertificateAndAuditLogData({
legacyDocumentId,
documentMeta: envelope.documentMeta,
settings,
});
for (const envelopeItem of envelopeItems) {
const result = await io.runTask(`decorate-${envelopeItem.id}`, async () => {
const newDocumentData: Array<{ oldDocumentDataId: string; newDocumentDataId: string }> = [];
for (const envelopeItem of envelopeItems) {
const envelopeItemFields = envelope.envelopeItems.find(
(item) => item.id === envelopeItem.id,
)?.field;
@ -235,7 +183,7 @@ export const run = async ({
throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
}
return decorateAndSignPdf({
const result = await decorateAndSignPdf({
envelope,
envelopeItem,
envelopeItemFields,
@ -244,25 +192,10 @@ export const run = async ({
certificateData,
auditLogData,
});
});
newDocumentData.push(result);
}
newDocumentData.push(result);
}
const postHog = PostHogServerClient();
if (postHog) {
postHog.capture({
distinctId: nanoid(),
event: 'App: Document Sealed',
properties: {
documentId: envelope.id,
isRejected,
},
});
}
await io.runTask('update-document', async () => {
await prisma.$transaction(async (tx) => {
for (const { oldDocumentDataId, newDocumentDataId } of newDocumentData) {
const newData = await tx.documentData.findFirstOrThrow({
@ -304,18 +237,24 @@ export const run = async ({
}),
});
});
return {
envelopeId: envelope.id,
envelopeStatus: envelope.status,
isRejected,
};
});
await io.runTask('send-completed-email', async () => {
let shouldSendCompletedEmail = sendEmail && !isResealing && !isRejected;
if (isResealing && !isDocumentCompleted(envelope.status)) {
if (isResealing && !isDocumentCompleted(envelopeStatus)) {
shouldSendCompletedEmail = sendEmail;
}
if (shouldSendCompletedEmail) {
await sendCompletedEmail({
id: { type: 'envelopeId', id: envelope.id },
id: { type: 'envelopeId', id: envelopeId },
requestMetadata,
});
}
@ -323,7 +262,7 @@ export const run = async ({
const updatedEnvelope = await prisma.envelope.findFirstOrThrow({
where: {
id: envelope.id,
id: envelopeId,
},
include: {
documentMeta: true,

View File

@ -39,7 +39,7 @@ export const RECIPIENT_COLOR_STYLES = {
baseRing: 'rgba(122, 195, 85, 1)',
baseRingHover: 'rgba(122, 195, 85, 0.3)',
baseTextHover: 'rgba(122, 195, 85, 1)',
fieldButton: 'hover:border-recipient-green bg-recipient-green/5 hover:bg-recipient-green/30 ',
fieldButton: 'hover:border-recipient-green hover:bg-recipient-green/30 ',
fieldItem: 'group/field-item rounded-[2px]',
fieldItemInitials: 'group-hover/field-item:bg-recipient-green',
comboxBoxTrigger:
@ -52,7 +52,7 @@ export const RECIPIENT_COLOR_STYLES = {
baseRing: 'rgba(56, 123, 199, 1)',
baseRingHover: 'rgba(56, 123, 199, 0.3)',
baseTextHover: 'rgba(56, 123, 199, 1)',
fieldButton: 'hover:border-recipient-blue bg-recipient-blue/5 hover:bg-recipient-blue/30',
fieldButton: 'hover:border-recipient-blue hover:bg-recipient-blue/30',
fieldItem: 'group/field-item rounded-[2px]',
fieldItemInitials: 'group-hover/field-item:bg-recipient-blue',
comboxBoxTrigger:
@ -65,7 +65,7 @@ export const RECIPIENT_COLOR_STYLES = {
baseRing: 'rgba(151, 71, 255, 1)',
baseRingHover: 'rgba(151, 71, 255, 0.3)',
baseTextHover: 'rgba(151, 71, 255, 1)',
fieldButton: 'hover:border-recipient-purple bg-recipient-purple/5 hover:bg-recipient-purple/30',
fieldButton: 'hover:border-recipient-purple hover:bg-recipient-purple/30',
fieldItem: 'group/field-item rounded-[2px]',
fieldItemInitials: 'group-hover/field-item:bg-recipient-purple',
comboxBoxTrigger:
@ -78,7 +78,7 @@ export const RECIPIENT_COLOR_STYLES = {
baseRing: 'rgba(246, 159, 30, 1)',
baseRingHover: 'rgba(246, 159, 30, 0.3)',
baseTextHover: 'rgba(246, 159, 30, 1)',
fieldButton: 'hover:border-recipient-orange bg-recipient-orange/5 hover:bg-recipient-orange/30',
fieldButton: 'hover:border-recipient-orange hover:bg-recipient-orange/30',
fieldItem: 'group/field-item rounded-[2px]',
fieldItemInitials: 'group-hover/field-item:bg-recipient-orange',
comboxBoxTrigger:
@ -91,7 +91,7 @@ export const RECIPIENT_COLOR_STYLES = {
baseRing: 'rgba(219, 186, 0, 1)',
baseRingHover: 'rgba(219, 186, 0, 0.3)',
baseTextHover: 'rgba(219, 186, 0, 1)',
fieldButton: 'hover:border-recipient-yellow bg-recipient-yellow/5 hover:bg-recipient-yellow/30',
fieldButton: 'hover:border-recipient-yellow hover:bg-recipient-yellow/30',
fieldItem: 'group/field-item rounded-[2px]',
fieldItemInitials: 'group-hover/field-item:bg-recipient-yellow',
comboxBoxTrigger:
@ -104,7 +104,7 @@ export const RECIPIENT_COLOR_STYLES = {
baseRing: 'rgba(217, 74, 186, 1)',
baseRingHover: 'rgba(217, 74, 186, 0.3)',
baseTextHover: 'rgba(217, 74, 186, 1)',
fieldButton: 'hover:border-recipient-pink bg-recipient-pink/5 hover:bg-recipient-pink/30',
fieldButton: 'hover:border-recipient-pink hover:bg-recipient-pink/30',
fieldItem: 'group/field-item rounded-[2px]',
fieldItemInitials: 'group-hover/field-item:bg-recipient-pink',
comboxBoxTrigger: