From db2f912a0845c04104c8ca9cbef48715450c99c3 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Wed, 5 Nov 2025 22:10:17 +1100 Subject: [PATCH] fix: update create envelope item endpoint to use formdata --- .../document-signing-page-view-v2.tsx | 1 - .../envelope-editor-recipient-form.tsx | 24 ++++++++-- .../envelope-editor-upload-page.tsx | 43 ++++++----------- .../e2e/api/v2/envelopes-api.spec.ts | 28 ++++------- .../providers/envelope-render-provider.tsx | 18 ++++---- .../get-envelope-for-recipient-signing.ts | 1 - packages/lib/types/envelope.ts | 15 ++---- .../envelope-router/create-envelope-items.ts | 46 ++++++------------- .../create-envelope-items.types.ts | 31 +++++-------- .../pdf-viewer/pdf-viewer-konva.tsx | 4 +- 10 files changed, 84 insertions(+), 127 deletions(-) diff --git a/apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx b/apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx index 83d5dc780..ee23842f3 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx @@ -207,7 +207,6 @@ export const DocumentSigningPageViewV2 = () => { ) : ( diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx index ef8a1288c..375524e68 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx @@ -482,30 +482,46 @@ export const EnvelopeEditorRecipientForm = () => { 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 hasAllowDictateNextSignerChanged = envelope.documentMeta.allowDictateNextSigner !== data.allowDictateNextSigner; const hasSignersChanged = - data.signers.length !== recipients.length || - data.signers.some((signer) => { + envelopeRecipients.length !== recipients.length || + envelopeRecipients.some((signer) => { const recipient = recipients.find((recipient) => recipient.id === signer.id); if (!recipient) { return true; } + const signerActionAuth = signer.actionAuth; + const recipientActionAuth = recipient.authOptions?.actionAuth || []; + return ( signer.email !== recipient.email || signer.name !== recipient.name || signer.role !== recipient.role || signer.signingOrder !== recipient.signingOrder || - !isDeepEqual(signer.actionAuth, recipient.authOptions?.actionAuth) + !isDeepEqual(signerActionAuth, recipientActionAuth) ); }); if (hasSignersChanged) { - setRecipientsDebounced(validatedFormValues.data.signers); + setRecipientsDebounced(envelopeRecipients); } if (hasSigningOrderChanged || hasAllowDictateNextSignerChanged) { diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx index 5eba88933..f203685b0 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx @@ -18,9 +18,9 @@ import { import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { nanoid } from '@documenso/lib/universal/id'; -import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope'; 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 { Card, @@ -114,36 +114,19 @@ export const EnvelopeEditorUploadPage = () => { setLocalFiles((prev) => [...prev, ...newUploadingFiles]); - const result = await Promise.all( - 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({ + const payload = { envelopeId: envelope.id, - data: envelopeItemsToCreate, - }).catch((error) => { + } satisfies TCreateEnvelopeItemsPayload; + + 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); // Set error state on files in batch upload. diff --git a/packages/app-tests/e2e/api/v2/envelopes-api.spec.ts b/packages/app-tests/e2e/api/v2/envelopes-api.spec.ts index ef9ffd049..8e759efd7 100644 --- a/packages/app-tests/e2e/api/v2/envelopes-api.spec.ts +++ b/packages/app-tests/e2e/api/v2/envelopes-api.spec.ts @@ -18,7 +18,7 @@ import { RecipientRole, } from '@documenso/prisma/client'; 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 { TCreateEnvelopePayload, TCreateEnvelopeResponse, @@ -403,28 +403,20 @@ test.describe('API V2 Envelopes', () => { expect(unauthRequest.status()).toBe(404); // Step 2: Create second envelope item via API - // Todo: Envelopes - Use API Route - const fieldMetaDocumentData = await prisma.documentData.create({ - data: { - type: 'BYTES_64', - data: fieldMetaPdf.toString('base64'), - initialData: fieldMetaPdf.toString('base64'), - }, - }); - - const createEnvelopeItemsRequest: TCreateEnvelopeItemsRequest = { + const createEnvelopeItemsPayload: TCreateEnvelopeItemsPayload = { 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`, { headers: { Authorization: `Bearer ${tokenA}` }, - data: createEnvelopeItemsRequest, + multipart: createEnvelopeItemFormData, }); expect(createItemsRes.ok()).toBeTruthy(); diff --git a/packages/lib/client-only/providers/envelope-render-provider.tsx b/packages/lib/client-only/providers/envelope-render-provider.tsx index 12fa53003..fdb8aeb72 100644 --- a/packages/lib/client-only/providers/envelope-render-provider.tsx +++ b/packages/lib/client-only/providers/envelope-render-provider.tsx @@ -23,7 +23,7 @@ type EnvelopeRenderOverrideSettings = { type EnvelopeRenderItem = TEnvelope['envelopeItems'][number]; type EnvelopeRenderProviderValue = { - getPdfBuffer: (documentDataId: string) => FileData | null; + getPdfBuffer: (envelopeItemId: string) => FileData | null; envelopeItems: EnvelopeRenderItem[]; currentEnvelopeItem: EnvelopeRenderItem | null; setCurrentEnvelopeItem: (envelopeItemId: string) => void; @@ -103,14 +103,14 @@ export const EnvelopeRenderProvider = ({ ); const loadEnvelopeItemPdfFile = async (envelopeItem: EnvelopeRenderItem) => { - if (files[envelopeItem.documentDataId]?.status === 'loading') { + if (files[envelopeItem.id]?.status === 'loading') { return; } - if (!files[envelopeItem.documentDataId]) { + if (!files[envelopeItem.id]) { setFiles((prev) => ({ ...prev, - [envelopeItem.documentDataId]: { + [envelopeItem.id]: { status: 'loading', }, })); @@ -129,7 +129,7 @@ export const EnvelopeRenderProvider = ({ setFiles((prev) => ({ ...prev, - [envelopeItem.documentDataId]: { + [envelopeItem.id]: { file: new Uint8Array(file), status: 'loaded', }, @@ -139,7 +139,7 @@ export const EnvelopeRenderProvider = ({ setFiles((prev) => ({ ...prev, - [envelopeItem.documentDataId]: { + [envelopeItem.id]: { status: 'error', }, })); @@ -147,8 +147,8 @@ export const EnvelopeRenderProvider = ({ }; const getPdfBuffer = useCallback( - (documentDataId: string) => { - return files[documentDataId] || null; + (envelopeItemId: string) => { + return files[envelopeItemId] || null; }, [files], ); @@ -168,7 +168,7 @@ export const EnvelopeRenderProvider = ({ // Look for any missing pdf files and load them. useEffect(() => { - const missingFiles = envelope.envelopeItems.filter((item) => !files[item.documentDataId]); + const missingFiles = envelope.envelopeItems.filter((item) => !files[item.id]); for (const item of missingFiles) { void loadEnvelopeItemPdfFile(item); diff --git a/packages/lib/server-only/envelope/get-envelope-for-recipient-signing.ts b/packages/lib/server-only/envelope/get-envelope-for-recipient-signing.ts index c9ea784f8..6bb6589e1 100644 --- a/packages/lib/server-only/envelope/get-envelope-for-recipient-signing.ts +++ b/packages/lib/server-only/envelope/get-envelope-for-recipient-signing.ts @@ -74,7 +74,6 @@ export const ZEnvelopeForSigningResponse = z.object({ envelopeId: true, id: true, title: true, - documentDataId: true, order: true, }).array(), diff --git a/packages/lib/types/envelope.ts b/packages/lib/types/envelope.ts index 9b424be91..a0b4cea62 100644 --- a/packages/lib/types/envelope.ts +++ b/packages/lib/types/envelope.ts @@ -6,8 +6,8 @@ import { EnvelopeSchema } from '@documenso/prisma/generated/zod/modelSchema/Enve import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import TemplateDirectLinkSchema from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema'; -import { ZFieldSchema } from './field'; -import { ZRecipientLiteSchema } from './recipient'; +import { ZEnvelopeFieldSchema } from './field'; +import { ZEnvelopeRecipientLiteSchema } from './recipient'; /** * The full envelope response schema. @@ -56,19 +56,12 @@ export const ZEnvelopeSchema = EnvelopeSchema.pick({ emailId: true, emailReplyTo: true, }), - recipients: ZRecipientLiteSchema.omit({ - documentId: true, - templateId: true, - }).array(), - fields: ZFieldSchema.omit({ - documentId: true, - templateId: true, - }).array(), + recipients: ZEnvelopeRecipientLiteSchema.array(), + fields: ZEnvelopeFieldSchema.array(), envelopeItems: EnvelopeItemSchema.pick({ envelopeId: true, id: true, title: true, - documentDataId: true, order: true, }).array(), directLink: TemplateDirectLinkSchema.pick({ diff --git a/packages/trpc/server/envelope-router/create-envelope-items.ts b/packages/trpc/server/envelope-router/create-envelope-items.ts index c223136e9..520138c92 100644 --- a/packages/trpc/server/envelope-router/create-envelope-items.ts +++ b/packages/trpc/server/envelope-router/create-envelope-items.ts @@ -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 { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; 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 { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope'; import { prisma } from '@documenso/prisma'; @@ -13,7 +14,6 @@ import { } from './create-envelope-items.types'; export const createEnvelopeItemsRoute = authenticatedProcedure - // Todo: Envelopes - Pending direct uploads .meta({ openapi: { method: 'POST', @@ -27,7 +27,8 @@ export const createEnvelopeItemsRoute = authenticatedProcedure .output(ZCreateEnvelopeItemsResponseSchema) .mutation(async ({ input, ctx }) => { const { user, teamId, metadata } = ctx; - const { envelopeId, data: items } = input; + const { payload, files } = input; + const { envelopeId } = payload; ctx.logger.info({ input: { @@ -81,7 +82,7 @@ export const createEnvelopeItemsRoute = authenticatedProcedure const organisationClaim = envelope.team.organisation.organisationClaim; const remainingEnvelopeItems = - organisationClaim.envelopeItemCount - envelope.envelopeItems.length - items.length; + organisationClaim.envelopeItemCount - envelope.envelopeItems.length - files.length; if (remainingEnvelopeItems < 0) { throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', { @@ -90,41 +91,24 @@ export const createEnvelopeItemsRoute = authenticatedProcedure }); } - const foundDocumentData = await prisma.documentData.findMany({ - where: { - id: { - in: items.map((item) => item.documentDataId), - }, - }, - select: { - envelopeItem: { - select: { - id: true, - }, - }, - }, - }); + // For each file, stream to s3 and create the document data. + const envelopeItems = await Promise.all( + files.map(async (file) => { + const { id: documentDataId } = await putNormalizedPdfFileServerSide(file); - // Check that all the document data was found. - if (foundDocumentData.length !== items.length) { - throw new AppError(AppErrorCode.NOT_FOUND, { - 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', - }); - } + return { + title: file.name, + documentDataId, + }; + }), + ); const currentHighestOrderValue = envelope.envelopeItems[envelope.envelopeItems.length - 1]?.order ?? 1; const result = await prisma.$transaction(async (tx) => { const createdItems = await tx.envelopeItem.createManyAndReturn({ - data: items.map((item) => ({ + data: envelopeItems.map((item) => ({ id: prefixedId('envelope_item'), envelopeId, title: item.title, diff --git a/packages/trpc/server/envelope-router/create-envelope-items.types.ts b/packages/trpc/server/envelope-router/create-envelope-items.types.ts index bc44b5948..2d7b002b9 100644 --- a/packages/trpc/server/envelope-router/create-envelope-items.types.ts +++ b/packages/trpc/server/envelope-router/create-envelope-items.types.ts @@ -1,38 +1,29 @@ 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 { 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(), - data: z - .object({ - title: ZDocumentTitleSchema, - documentDataId: z.string(), - }) - .array(), + // data: z.object() // Currently not used. +}); + +export const ZCreateEnvelopeItemsRequestSchema = zodFormData({ + payload: zfd.json(ZCreateEnvelopeItemsPayloadSchema), + files: zfd.repeatableOfType(zfd.file()), }); export const ZCreateEnvelopeItemsResponseSchema = z.object({ createdEnvelopeItems: EnvelopeItemSchema.pick({ id: true, title: true, - documentDataId: true, envelopeId: true, order: true, - }) - .extend({ - documentData: DocumentDataSchema.pick({ - type: true, - id: true, - data: true, - initialData: true, - }), - }) - .array(), + }).array(), }); +export type TCreateEnvelopeItemsPayload = z.infer; export type TCreateEnvelopeItemsRequest = z.infer; export type TCreateEnvelopeItemsResponse = z.infer; diff --git a/packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx b/packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx index 32e73e6b5..1c0fc37a8 100644 --- a/packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx +++ b/packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx @@ -78,7 +78,7 @@ export const PdfViewerKonva = ({ const [pdfError, setPdfError] = useState(false); const envelopeItemFile = useMemo(() => { - const data = getPdfBuffer(currentEnvelopeItem?.documentDataId || ''); + const data = getPdfBuffer(currentEnvelopeItem?.id || ''); if (!data || data.status !== 'loaded') { return null; @@ -87,7 +87,7 @@ export const PdfViewerKonva = ({ return { data: new Uint8Array(data.file), }; - }, [currentEnvelopeItem?.documentDataId, getPdfBuffer]); + }, [currentEnvelopeItem?.id, getPdfBuffer]); const onDocumentLoaded = useCallback( (doc: PDFDocumentProxy) => {