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) => {