mirror of
https://github.com/documenso/documenso.git
synced 2026-07-24 08:54:20 +10:00
feat: remove email requirement for recipients (#2040)
This commit is contained in:
@@ -1,23 +1,56 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
export const useAutoSave = <T>(onSave: (data: T) => Promise<void>) => {
|
||||
const saveTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
type SaveRequest<T, R> = {
|
||||
data: T;
|
||||
onResponse?: (response: R) => void;
|
||||
};
|
||||
|
||||
const saveFormData = async (data: T) => {
|
||||
try {
|
||||
await onSave(data);
|
||||
} catch (error) {
|
||||
console.error('Auto-save failed:', error);
|
||||
export const useAutoSave = <T, R = void>(
|
||||
onSave: (data: T) => Promise<R>,
|
||||
options: { delay?: number } = {},
|
||||
) => {
|
||||
const { delay = 2000 } = options;
|
||||
|
||||
const saveTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const saveQueueRef = useRef<SaveRequest<T, R>[]>([]);
|
||||
const isProcessingRef = useRef(false);
|
||||
|
||||
const processQueue = async () => {
|
||||
if (isProcessingRef.current || saveQueueRef.current.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
isProcessingRef.current = true;
|
||||
|
||||
while (saveQueueRef.current.length > 0) {
|
||||
const request = saveQueueRef.current.shift()!;
|
||||
|
||||
try {
|
||||
const response = await onSave(request.data);
|
||||
request.onResponse?.(response);
|
||||
} catch (error) {
|
||||
console.error('Auto-save failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
isProcessingRef.current = false;
|
||||
};
|
||||
|
||||
const scheduleSave = useCallback((data: T) => {
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
}
|
||||
const saveFormData = async (data: T, onResponse?: (response: R) => void) => {
|
||||
saveQueueRef.current.push({ data, onResponse });
|
||||
await processQueue();
|
||||
};
|
||||
|
||||
saveTimeoutRef.current = setTimeout(() => void saveFormData(data), 2000);
|
||||
}, []);
|
||||
const scheduleSave = useCallback(
|
||||
(data: T, onResponse?: (response: R) => void) => {
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
}
|
||||
|
||||
saveTimeoutRef.current = setTimeout(() => void saveFormData(data, onResponse), delay);
|
||||
},
|
||||
[delay],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
||||
@@ -84,9 +84,7 @@ export const setFieldsForDocument = async ({
|
||||
const linkedFields = fields.map((field) => {
|
||||
const existing = existingFields.find((existingField) => existingField.id === field.id);
|
||||
|
||||
const recipient = document.recipients.find(
|
||||
(recipient) => recipient.email.toLowerCase() === field.signerEmail.toLowerCase(),
|
||||
);
|
||||
const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
@@ -226,10 +224,8 @@ export const setFieldsForDocument = async ({
|
||||
},
|
||||
recipient: {
|
||||
connect: {
|
||||
documentId_email: {
|
||||
documentId,
|
||||
email: fieldSignerEmail,
|
||||
},
|
||||
id: field.recipientId,
|
||||
documentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -330,6 +326,7 @@ type FieldData = {
|
||||
id?: number | null;
|
||||
type: FieldType;
|
||||
signerEmail: string;
|
||||
recipientId: number;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
|
||||
@@ -26,6 +26,7 @@ export type SetFieldsForTemplateOptions = {
|
||||
id?: number | null;
|
||||
type: FieldType;
|
||||
signerEmail: string;
|
||||
recipientId: number;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
@@ -169,10 +170,8 @@ export const setFieldsForTemplate = async ({
|
||||
},
|
||||
recipient: {
|
||||
connect: {
|
||||
templateId_email: {
|
||||
templateId,
|
||||
email: field.signerEmail.toLowerCase(),
|
||||
},
|
||||
id: field.recipientId,
|
||||
templateId,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -85,20 +85,6 @@ export const createDocumentRecipients = async ({
|
||||
email: recipient.email.toLowerCase(),
|
||||
}));
|
||||
|
||||
const duplicateRecipients = normalizedRecipients.filter((newRecipient) => {
|
||||
const existingRecipient = document.recipients.find(
|
||||
(existingRecipient) => existingRecipient.email === newRecipient.email,
|
||||
);
|
||||
|
||||
return existingRecipient !== undefined;
|
||||
});
|
||||
|
||||
if (duplicateRecipients.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Duplicate recipient(s) found for ${duplicateRecipients.map((recipient) => recipient.email).join(', ')}`,
|
||||
});
|
||||
}
|
||||
|
||||
const createdRecipients = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
normalizedRecipients.map(async (recipient) => {
|
||||
|
||||
@@ -71,20 +71,6 @@ export const createTemplateRecipients = async ({
|
||||
email: recipient.email.toLowerCase(),
|
||||
}));
|
||||
|
||||
const duplicateRecipients = normalizedRecipients.filter((newRecipient) => {
|
||||
const existingRecipient = template.recipients.find(
|
||||
(existingRecipient) => existingRecipient.email === newRecipient.email,
|
||||
);
|
||||
|
||||
return existingRecipient !== undefined;
|
||||
});
|
||||
|
||||
if (duplicateRecipients.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Duplicate recipient(s) found for ${duplicateRecipients.map((recipient) => recipient.email).join(', ')}`,
|
||||
});
|
||||
}
|
||||
|
||||
const createdRecipients = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
normalizedRecipients.map(async (recipient) => {
|
||||
|
||||
@@ -122,16 +122,12 @@ export const setDocumentRecipients = async ({
|
||||
|
||||
const removedRecipients = existingRecipients.filter(
|
||||
(existingRecipient) =>
|
||||
!normalizedRecipients.find(
|
||||
(recipient) =>
|
||||
recipient.id === existingRecipient.id || recipient.email === existingRecipient.email,
|
||||
),
|
||||
!normalizedRecipients.find((recipient) => recipient.id === existingRecipient.id),
|
||||
);
|
||||
|
||||
const linkedRecipients = normalizedRecipients.map((recipient) => {
|
||||
const existing = existingRecipients.find(
|
||||
(existingRecipient) =>
|
||||
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
|
||||
(existingRecipient) => existingRecipient.id === recipient.id,
|
||||
);
|
||||
|
||||
const canPersistedRecipientBeModified =
|
||||
|
||||
@@ -94,10 +94,7 @@ export const setTemplateRecipients = async ({
|
||||
|
||||
const removedRecipients = existingRecipients.filter(
|
||||
(existingRecipient) =>
|
||||
!normalizedRecipients.find(
|
||||
(recipient) =>
|
||||
recipient.id === existingRecipient.id || recipient.email === existingRecipient.email,
|
||||
),
|
||||
!normalizedRecipients.find((recipient) => recipient.id === existingRecipient.id),
|
||||
);
|
||||
|
||||
if (template.directLink !== null) {
|
||||
@@ -124,8 +121,7 @@ export const setTemplateRecipients = async ({
|
||||
|
||||
const linkedRecipients = normalizedRecipients.map((recipient) => {
|
||||
const existing = existingRecipients.find(
|
||||
(existingRecipient) =>
|
||||
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
|
||||
(existingRecipient) => existingRecipient.id === recipient.id,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -91,17 +91,6 @@ export const updateDocumentRecipients = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const duplicateRecipientWithSameEmail = document.recipients.find(
|
||||
(existingRecipient) =>
|
||||
existingRecipient.email === recipient.email && existingRecipient.id !== recipient.id,
|
||||
);
|
||||
|
||||
if (duplicateRecipientWithSameEmail) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Duplicate recipient with the same email found: ${duplicateRecipientWithSameEmail.email}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!canRecipientBeModified(originalRecipient, document.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Cannot modify a recipient who has already interacted with the document',
|
||||
|
||||
@@ -80,17 +80,6 @@ export const updateTemplateRecipients = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const duplicateRecipientWithSameEmail = template.recipients.find(
|
||||
(existingRecipient) =>
|
||||
existingRecipient.email === recipient.email && existingRecipient.id !== recipient.id,
|
||||
);
|
||||
|
||||
if (duplicateRecipientWithSameEmail) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Duplicate recipient with the same email found: ${duplicateRecipientWithSameEmail.email}`,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
originalRecipient,
|
||||
recipientUpdateData: recipient,
|
||||
|
||||
@@ -19,6 +19,8 @@ export type CreateDocumentFromTemplateLegacyOptions = {
|
||||
}[];
|
||||
};
|
||||
|
||||
// !TODO: Make this work
|
||||
|
||||
/**
|
||||
* Legacy server function for /api/v1
|
||||
*/
|
||||
@@ -58,6 +60,15 @@ export const createDocumentFromTemplateLegacy = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const recipientsToCreate = template.recipients.map((recipient) => ({
|
||||
id: recipient.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
}));
|
||||
|
||||
const document = await prisma.document.create({
|
||||
data: {
|
||||
qrToken: prefixedId('qr'),
|
||||
@@ -70,12 +81,12 @@ export const createDocumentFromTemplateLegacy = async ({
|
||||
documentDataId: documentData.id,
|
||||
useLegacyFieldInsertion: template.useLegacyFieldInsertion ?? false,
|
||||
recipients: {
|
||||
create: template.recipients.map((recipient) => ({
|
||||
create: recipientsToCreate.map((recipient) => ({
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
token: recipient.token,
|
||||
})),
|
||||
},
|
||||
documentMeta: {
|
||||
@@ -95,9 +106,11 @@ export const createDocumentFromTemplateLegacy = async ({
|
||||
|
||||
await prisma.field.createMany({
|
||||
data: template.fields.map((field) => {
|
||||
const recipient = template.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
const recipient = recipientsToCreate.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
const documentRecipient = document.recipients.find((doc) => doc.email === recipient?.email);
|
||||
const documentRecipient = document.recipients.find(
|
||||
(documentRecipient) => documentRecipient.token === recipient?.token,
|
||||
);
|
||||
|
||||
if (!documentRecipient) {
|
||||
throw new Error('Recipient not found.');
|
||||
@@ -118,28 +131,32 @@ export const createDocumentFromTemplateLegacy = async ({
|
||||
}),
|
||||
});
|
||||
|
||||
// Replicate the old logic, get by index and create if we exceed the number of existing recipients.
|
||||
if (recipients && recipients.length > 0) {
|
||||
document.recipients = await Promise.all(
|
||||
await Promise.all(
|
||||
recipients.map(async (recipient, index) => {
|
||||
const existingRecipient = document.recipients.at(index);
|
||||
|
||||
return await prisma.recipient.upsert({
|
||||
where: {
|
||||
documentId_email: {
|
||||
if (existingRecipient) {
|
||||
return await prisma.recipient.update({
|
||||
where: {
|
||||
id: existingRecipient.id,
|
||||
documentId: document.id,
|
||||
email: existingRecipient?.email ?? recipient.email,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
},
|
||||
create: {
|
||||
data: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.recipient.create({
|
||||
data: {
|
||||
documentId: document.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
@@ -149,5 +166,18 @@ export const createDocumentFromTemplateLegacy = async ({
|
||||
);
|
||||
}
|
||||
|
||||
return document;
|
||||
// Gross but we need to do the additional fetch since we mutate above.
|
||||
const updatedRecipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...document,
|
||||
recipients: updatedRecipients,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
type FinalRecipient = Pick<
|
||||
Recipient,
|
||||
'name' | 'email' | 'role' | 'authOptions' | 'signingOrder'
|
||||
'name' | 'email' | 'role' | 'authOptions' | 'signingOrder' | 'token'
|
||||
> & {
|
||||
templateRecipientId: number;
|
||||
fields: Field[];
|
||||
@@ -350,6 +350,7 @@ export const createDocumentFromTemplate = async ({
|
||||
role: templateRecipient.role,
|
||||
signingOrder: foundRecipient?.signingOrder ?? templateRecipient.signingOrder,
|
||||
authOptions: templateRecipient.authOptions,
|
||||
token: nanoid(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -441,7 +442,7 @@ export const createDocumentFromTemplate = async ({
|
||||
? SigningStatus.SIGNED
|
||||
: SigningStatus.NOT_SIGNED,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
token: recipient.token,
|
||||
};
|
||||
}),
|
||||
},
|
||||
@@ -500,8 +501,8 @@ export const createDocumentFromTemplate = async ({
|
||||
}
|
||||
}
|
||||
|
||||
Object.values(finalRecipients).forEach(({ email, fields }) => {
|
||||
const recipient = document.recipients.find((recipient) => recipient.email === email);
|
||||
Object.values(finalRecipients).forEach(({ token, fields }) => {
|
||||
const recipient = document.recipients.find((recipient) => recipient.token === token);
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found.');
|
||||
|
||||
Reference in New Issue
Block a user