fix: update create envelope item endpoint to use formdata

This commit is contained in:
David Nguyen
2025-11-05 22:10:17 +11:00
parent fc2e9af6a0
commit db2f912a08
10 changed files with 84 additions and 127 deletions

View File

@ -207,7 +207,6 @@ export const DocumentSigningPageViewV2 = () => {
<PDFViewerKonvaLazy <PDFViewerKonvaLazy
renderer="signing" renderer="signing"
key={currentEnvelopeItem.id} key={currentEnvelopeItem.id}
documentDataId={currentEnvelopeItem.documentDataId}
customPageRenderer={EnvelopeSignerPageRenderer} customPageRenderer={EnvelopeSignerPageRenderer}
/> />
) : ( ) : (

View File

@ -482,30 +482,46 @@ export const EnvelopeEditorRecipientForm = () => {
const { data } = validatedFormValues; const { data } = validatedFormValues;
// Weird edge case where the whole envelope is created via API
// with no signing order. If they come to this page it will show an error
// since they aren't equal and the recipient is no longer editable.
const envelopeRecipients = data.signers.map((recipient) => {
if (!canRecipientBeModified(recipient.id)) {
return {
...recipient,
signingOrder: recipient.signingOrder,
};
}
return recipient;
});
const hasSigningOrderChanged = envelope.documentMeta.signingOrder !== data.signingOrder; const hasSigningOrderChanged = envelope.documentMeta.signingOrder !== data.signingOrder;
const hasAllowDictateNextSignerChanged = const hasAllowDictateNextSignerChanged =
envelope.documentMeta.allowDictateNextSigner !== data.allowDictateNextSigner; envelope.documentMeta.allowDictateNextSigner !== data.allowDictateNextSigner;
const hasSignersChanged = const hasSignersChanged =
data.signers.length !== recipients.length || envelopeRecipients.length !== recipients.length ||
data.signers.some((signer) => { envelopeRecipients.some((signer) => {
const recipient = recipients.find((recipient) => recipient.id === signer.id); const recipient = recipients.find((recipient) => recipient.id === signer.id);
if (!recipient) { if (!recipient) {
return true; return true;
} }
const signerActionAuth = signer.actionAuth;
const recipientActionAuth = recipient.authOptions?.actionAuth || [];
return ( return (
signer.email !== recipient.email || signer.email !== recipient.email ||
signer.name !== recipient.name || signer.name !== recipient.name ||
signer.role !== recipient.role || signer.role !== recipient.role ||
signer.signingOrder !== recipient.signingOrder || signer.signingOrder !== recipient.signingOrder ||
!isDeepEqual(signer.actionAuth, recipient.authOptions?.actionAuth) !isDeepEqual(signerActionAuth, recipientActionAuth)
); );
}); });
if (hasSignersChanged) { if (hasSignersChanged) {
setRecipientsDebounced(validatedFormValues.data.signers); setRecipientsDebounced(envelopeRecipients);
} }
if (hasSigningOrderChanged || hasAllowDictateNextSignerChanged) { if (hasSigningOrderChanged || hasAllowDictateNextSignerChanged) {

View File

@ -18,9 +18,9 @@ import {
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { nanoid } from '@documenso/lib/universal/id'; import { nanoid } from '@documenso/lib/universal/id';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope'; import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.types';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
Card, Card,
@ -114,36 +114,19 @@ export const EnvelopeEditorUploadPage = () => {
setLocalFiles((prev) => [...prev, ...newUploadingFiles]); setLocalFiles((prev) => [...prev, ...newUploadingFiles]);
const result = await Promise.all( const payload = {
files.map(async (file, index) => {
try {
const response = await putPdfFile(file);
// Mark as uploaded (remove from uploading state)
return {
title: file.name,
documentDataId: response.id,
};
} catch (_error) {
setLocalFiles((prev) =>
prev.map((uploadingFile) =>
uploadingFile.id === newUploadingFiles[index].id
? { ...uploadingFile, isError: true, isUploading: false }
: uploadingFile,
),
);
}
}),
);
const envelopeItemsToCreate = result.filter(
(item): item is { title: string; documentDataId: string } => item !== undefined,
);
const { createdEnvelopeItems } = await createEnvelopeItems({
envelopeId: envelope.id, envelopeId: envelope.id,
data: envelopeItemsToCreate, } satisfies TCreateEnvelopeItemsPayload;
}).catch((error) => {
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
for (const file of files) {
formData.append('files', file);
}
const { createdEnvelopeItems } = await createEnvelopeItems(formData).catch((error) => {
console.error(error); console.error(error);
// Set error state on files in batch upload. // Set error state on files in batch upload.

View File

@ -18,7 +18,7 @@ import {
RecipientRole, RecipientRole,
} from '@documenso/prisma/client'; } from '@documenso/prisma/client';
import { seedUser } from '@documenso/prisma/seed/users'; import { seedUser } from '@documenso/prisma/seed/users';
import type { TCreateEnvelopeItemsRequest } from '@documenso/trpc/server/envelope-router/create-envelope-items.types'; import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.types';
import type { import type {
TCreateEnvelopePayload, TCreateEnvelopePayload,
TCreateEnvelopeResponse, TCreateEnvelopeResponse,
@ -403,28 +403,20 @@ test.describe('API V2 Envelopes', () => {
expect(unauthRequest.status()).toBe(404); expect(unauthRequest.status()).toBe(404);
// Step 2: Create second envelope item via API // Step 2: Create second envelope item via API
// Todo: Envelopes - Use API Route const createEnvelopeItemsPayload: TCreateEnvelopeItemsPayload = {
const fieldMetaDocumentData = await prisma.documentData.create({
data: {
type: 'BYTES_64',
data: fieldMetaPdf.toString('base64'),
initialData: fieldMetaPdf.toString('base64'),
},
});
const createEnvelopeItemsRequest: TCreateEnvelopeItemsRequest = {
envelopeId: createdEnvelope.id, envelopeId: createdEnvelope.id,
data: [
{
title: 'Field Meta Test',
documentDataId: fieldMetaDocumentData.id,
},
],
}; };
const createEnvelopeItemFormData = new FormData();
createEnvelopeItemFormData.append('payload', JSON.stringify(createEnvelopeItemsPayload));
createEnvelopeItemFormData.append(
'files',
new File([fieldMetaPdf], 'field-meta.pdf', { type: 'application/pdf' }),
);
const createItemsRes = await request.post(`${baseUrl}/envelope/item/create-many`, { const createItemsRes = await request.post(`${baseUrl}/envelope/item/create-many`, {
headers: { Authorization: `Bearer ${tokenA}` }, headers: { Authorization: `Bearer ${tokenA}` },
data: createEnvelopeItemsRequest, multipart: createEnvelopeItemFormData,
}); });
expect(createItemsRes.ok()).toBeTruthy(); expect(createItemsRes.ok()).toBeTruthy();

View File

@ -23,7 +23,7 @@ type EnvelopeRenderOverrideSettings = {
type EnvelopeRenderItem = TEnvelope['envelopeItems'][number]; type EnvelopeRenderItem = TEnvelope['envelopeItems'][number];
type EnvelopeRenderProviderValue = { type EnvelopeRenderProviderValue = {
getPdfBuffer: (documentDataId: string) => FileData | null; getPdfBuffer: (envelopeItemId: string) => FileData | null;
envelopeItems: EnvelopeRenderItem[]; envelopeItems: EnvelopeRenderItem[];
currentEnvelopeItem: EnvelopeRenderItem | null; currentEnvelopeItem: EnvelopeRenderItem | null;
setCurrentEnvelopeItem: (envelopeItemId: string) => void; setCurrentEnvelopeItem: (envelopeItemId: string) => void;
@ -103,14 +103,14 @@ export const EnvelopeRenderProvider = ({
); );
const loadEnvelopeItemPdfFile = async (envelopeItem: EnvelopeRenderItem) => { const loadEnvelopeItemPdfFile = async (envelopeItem: EnvelopeRenderItem) => {
if (files[envelopeItem.documentDataId]?.status === 'loading') { if (files[envelopeItem.id]?.status === 'loading') {
return; return;
} }
if (!files[envelopeItem.documentDataId]) { if (!files[envelopeItem.id]) {
setFiles((prev) => ({ setFiles((prev) => ({
...prev, ...prev,
[envelopeItem.documentDataId]: { [envelopeItem.id]: {
status: 'loading', status: 'loading',
}, },
})); }));
@ -129,7 +129,7 @@ export const EnvelopeRenderProvider = ({
setFiles((prev) => ({ setFiles((prev) => ({
...prev, ...prev,
[envelopeItem.documentDataId]: { [envelopeItem.id]: {
file: new Uint8Array(file), file: new Uint8Array(file),
status: 'loaded', status: 'loaded',
}, },
@ -139,7 +139,7 @@ export const EnvelopeRenderProvider = ({
setFiles((prev) => ({ setFiles((prev) => ({
...prev, ...prev,
[envelopeItem.documentDataId]: { [envelopeItem.id]: {
status: 'error', status: 'error',
}, },
})); }));
@ -147,8 +147,8 @@ export const EnvelopeRenderProvider = ({
}; };
const getPdfBuffer = useCallback( const getPdfBuffer = useCallback(
(documentDataId: string) => { (envelopeItemId: string) => {
return files[documentDataId] || null; return files[envelopeItemId] || null;
}, },
[files], [files],
); );
@ -168,7 +168,7 @@ export const EnvelopeRenderProvider = ({
// Look for any missing pdf files and load them. // Look for any missing pdf files and load them.
useEffect(() => { useEffect(() => {
const missingFiles = envelope.envelopeItems.filter((item) => !files[item.documentDataId]); const missingFiles = envelope.envelopeItems.filter((item) => !files[item.id]);
for (const item of missingFiles) { for (const item of missingFiles) {
void loadEnvelopeItemPdfFile(item); void loadEnvelopeItemPdfFile(item);

View File

@ -74,7 +74,6 @@ export const ZEnvelopeForSigningResponse = z.object({
envelopeId: true, envelopeId: true,
id: true, id: true,
title: true, title: true,
documentDataId: true,
order: true, order: true,
}).array(), }).array(),

View File

@ -6,8 +6,8 @@ import { EnvelopeSchema } from '@documenso/prisma/generated/zod/modelSchema/Enve
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import TemplateDirectLinkSchema from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema'; import TemplateDirectLinkSchema from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema';
import { ZFieldSchema } from './field'; import { ZEnvelopeFieldSchema } from './field';
import { ZRecipientLiteSchema } from './recipient'; import { ZEnvelopeRecipientLiteSchema } from './recipient';
/** /**
* The full envelope response schema. * The full envelope response schema.
@ -56,19 +56,12 @@ export const ZEnvelopeSchema = EnvelopeSchema.pick({
emailId: true, emailId: true,
emailReplyTo: true, emailReplyTo: true,
}), }),
recipients: ZRecipientLiteSchema.omit({ recipients: ZEnvelopeRecipientLiteSchema.array(),
documentId: true, fields: ZEnvelopeFieldSchema.array(),
templateId: true,
}).array(),
fields: ZFieldSchema.omit({
documentId: true,
templateId: true,
}).array(),
envelopeItems: EnvelopeItemSchema.pick({ envelopeItems: EnvelopeItemSchema.pick({
envelopeId: true, envelopeId: true,
id: true, id: true,
title: true, title: true,
documentDataId: true,
order: true, order: true,
}).array(), }).array(),
directLink: TemplateDirectLinkSchema.pick({ directLink: TemplateDirectLinkSchema.pick({

View File

@ -2,6 +2,7 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id'; import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { prefixedId } from '@documenso/lib/universal/id'; import { prefixedId } from '@documenso/lib/universal/id';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope'; import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@ -13,7 +14,6 @@ import {
} from './create-envelope-items.types'; } from './create-envelope-items.types';
export const createEnvelopeItemsRoute = authenticatedProcedure export const createEnvelopeItemsRoute = authenticatedProcedure
// Todo: Envelopes - Pending direct uploads
.meta({ .meta({
openapi: { openapi: {
method: 'POST', method: 'POST',
@ -27,7 +27,8 @@ export const createEnvelopeItemsRoute = authenticatedProcedure
.output(ZCreateEnvelopeItemsResponseSchema) .output(ZCreateEnvelopeItemsResponseSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { user, teamId, metadata } = ctx; const { user, teamId, metadata } = ctx;
const { envelopeId, data: items } = input; const { payload, files } = input;
const { envelopeId } = payload;
ctx.logger.info({ ctx.logger.info({
input: { input: {
@ -81,7 +82,7 @@ export const createEnvelopeItemsRoute = authenticatedProcedure
const organisationClaim = envelope.team.organisation.organisationClaim; const organisationClaim = envelope.team.organisation.organisationClaim;
const remainingEnvelopeItems = const remainingEnvelopeItems =
organisationClaim.envelopeItemCount - envelope.envelopeItems.length - items.length; organisationClaim.envelopeItemCount - envelope.envelopeItems.length - files.length;
if (remainingEnvelopeItems < 0) { if (remainingEnvelopeItems < 0) {
throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', { throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', {
@ -90,41 +91,24 @@ export const createEnvelopeItemsRoute = authenticatedProcedure
}); });
} }
const foundDocumentData = await prisma.documentData.findMany({ // For each file, stream to s3 and create the document data.
where: { const envelopeItems = await Promise.all(
id: { files.map(async (file) => {
in: items.map((item) => item.documentDataId), const { id: documentDataId } = await putNormalizedPdfFileServerSide(file);
},
},
select: {
envelopeItem: {
select: {
id: true,
},
},
},
});
// Check that all the document data was found. return {
if (foundDocumentData.length !== items.length) { title: file.name,
throw new AppError(AppErrorCode.NOT_FOUND, { documentDataId,
message: 'Document data not found', };
}); }),
} );
// Check that it doesn't already have an envelope item.
if (foundDocumentData.some((documentData) => documentData.envelopeItem?.id)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document data not found',
});
}
const currentHighestOrderValue = const currentHighestOrderValue =
envelope.envelopeItems[envelope.envelopeItems.length - 1]?.order ?? 1; envelope.envelopeItems[envelope.envelopeItems.length - 1]?.order ?? 1;
const result = await prisma.$transaction(async (tx) => { const result = await prisma.$transaction(async (tx) => {
const createdItems = await tx.envelopeItem.createManyAndReturn({ const createdItems = await tx.envelopeItem.createManyAndReturn({
data: items.map((item) => ({ data: envelopeItems.map((item) => ({
id: prefixedId('envelope_item'), id: prefixedId('envelope_item'),
envelopeId, envelopeId,
title: item.title, title: item.title,

View File

@ -1,38 +1,29 @@
import { z } from 'zod'; import { z } from 'zod';
import { zfd } from 'zod-form-data';
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema'; import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
import { ZDocumentTitleSchema } from '../document-router/schema'; import { zodFormData } from '../../utils/zod-form-data';
export const ZCreateEnvelopeItemsRequestSchema = z.object({ export const ZCreateEnvelopeItemsPayloadSchema = z.object({
envelopeId: z.string(), envelopeId: z.string(),
data: z // data: z.object() // Currently not used.
.object({ });
title: ZDocumentTitleSchema,
documentDataId: z.string(), export const ZCreateEnvelopeItemsRequestSchema = zodFormData({
}) payload: zfd.json(ZCreateEnvelopeItemsPayloadSchema),
.array(), files: zfd.repeatableOfType(zfd.file()),
}); });
export const ZCreateEnvelopeItemsResponseSchema = z.object({ export const ZCreateEnvelopeItemsResponseSchema = z.object({
createdEnvelopeItems: EnvelopeItemSchema.pick({ createdEnvelopeItems: EnvelopeItemSchema.pick({
id: true, id: true,
title: true, title: true,
documentDataId: true,
envelopeId: true, envelopeId: true,
order: true, order: true,
}) }).array(),
.extend({
documentData: DocumentDataSchema.pick({
type: true,
id: true,
data: true,
initialData: true,
}),
})
.array(),
}); });
export type TCreateEnvelopeItemsPayload = z.infer<typeof ZCreateEnvelopeItemsPayloadSchema>;
export type TCreateEnvelopeItemsRequest = z.infer<typeof ZCreateEnvelopeItemsRequestSchema>; export type TCreateEnvelopeItemsRequest = z.infer<typeof ZCreateEnvelopeItemsRequestSchema>;
export type TCreateEnvelopeItemsResponse = z.infer<typeof ZCreateEnvelopeItemsResponseSchema>; export type TCreateEnvelopeItemsResponse = z.infer<typeof ZCreateEnvelopeItemsResponseSchema>;

View File

@ -78,7 +78,7 @@ export const PdfViewerKonva = ({
const [pdfError, setPdfError] = useState(false); const [pdfError, setPdfError] = useState(false);
const envelopeItemFile = useMemo(() => { const envelopeItemFile = useMemo(() => {
const data = getPdfBuffer(currentEnvelopeItem?.documentDataId || ''); const data = getPdfBuffer(currentEnvelopeItem?.id || '');
if (!data || data.status !== 'loaded') { if (!data || data.status !== 'loaded') {
return null; return null;
@ -87,7 +87,7 @@ export const PdfViewerKonva = ({
return { return {
data: new Uint8Array(data.file), data: new Uint8Array(data.file),
}; };
}, [currentEnvelopeItem?.documentDataId, getPdfBuffer]); }, [currentEnvelopeItem?.id, getPdfBuffer]);
const onDocumentLoaded = useCallback( const onDocumentLoaded = useCallback(
(doc: PDFDocumentProxy) => { (doc: PDFDocumentProxy) => {