diff --git a/apps/remix/app/components/general/document/document-upload-button-legacy.tsx b/apps/remix/app/components/general/document/document-upload-button-legacy.tsx index d306e986a..f4f13aaf0 100644 --- a/apps/remix/app/components/general/document/document-upload-button-legacy.tsx +++ b/apps/remix/app/components/general/document/document-upload-button-legacy.tsx @@ -76,8 +76,10 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe const payload = { title: file.name, - timezone: userTimezone, folderId: folderId ?? undefined, + meta: { + timezone: userTimezone, + }, } satisfies TCreateDocumentPayloadSchema; const formData = new FormData(); diff --git a/packages/app-tests/e2e/folders/team-account-folders.spec.ts b/packages/app-tests/e2e/folders/team-account-folders.spec.ts index 08ca3e2e3..25b924c9f 100644 --- a/packages/app-tests/e2e/folders/team-account-folders.spec.ts +++ b/packages/app-tests/e2e/folders/team-account-folders.spec.ts @@ -9,6 +9,7 @@ import { seedTeamMember } from '@documenso/prisma/seed/teams'; import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; import { apiSignin } from '../fixtures/authentication'; +import { expectTextToBeVisible } from '../fixtures/generic'; test.describe.configure({ mode: 'parallel' }); @@ -81,20 +82,23 @@ test('[TEAMS]: can create a document inside a document folder', async ({ page }) redirectPath: `/t/${team.url}/documents/f/${teamFolder.id}`, }); - const fileInput = page.locator('input[type="file"]').nth(2); - await fileInput.waitFor({ state: 'attached' }); + // Upload document. + const [fileChooser] = await Promise.all([ + page.waitForEvent('filechooser'), + page.getByRole('button', { name: 'Document (Legacy)' }).click(), + ]); - await fileInput.setInputFiles( + await fileChooser.setFiles( path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'), ); await page.waitForTimeout(3000); - await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); + await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf'); await page.goto(`/t/${team.url}/documents/f/${teamFolder.id}`); - await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); + await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf'); }); test('[TEAMS]: can pin a document folder', async ({ page }) => { @@ -382,11 +386,11 @@ test('[TEAMS]: can create a template inside a template folder', async ({ page }) await page.waitForTimeout(3000); // Expect redirect. - await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); + await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf'); // Return to folder and verify file is visible. await page.goto(`/t/${team.url}/templates/f/${folder.id}`); - await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); + await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf'); }); test('[TEAMS]: can pin a template folder', async ({ page }) => { @@ -851,7 +855,7 @@ test('[TEAMS]: documents inherit folder visibility', async ({ page }) => { await page.waitForTimeout(3000); - await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); + await expectTextToBeVisible(page, 'documenso-supporter-pledge.pdf'); await expect(page.getByRole('combobox').filter({ hasText: 'Admins only' })).toBeVisible(); }); diff --git a/packages/trpc/server/document-router/create-document.ts b/packages/trpc/server/document-router/create-document.ts index a5f915ffb..a515db87f 100644 --- a/packages/trpc/server/document-router/create-document.ts +++ b/packages/trpc/server/document-router/create-document.ts @@ -3,6 +3,7 @@ import { EnvelopeType } from '@prisma/client'; import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; +import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf'; import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server'; import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; @@ -22,9 +23,34 @@ export const createDocumentRoute = authenticatedProcedure const { payload, file } = input; - const { title, timezone, folderId, attachments } = payload; + const { + title, + externalId, + visibility, + globalAccessAuth, + globalActionAuth, + recipients, + meta, + folderId, + formValues, + attachments, + } = payload; - const { id: documentDataId } = await putNormalizedPdfFileServerSide(file); + let pdf = Buffer.from(await file.arrayBuffer()); + + if (formValues) { + // eslint-disable-next-line require-atomic-updates + pdf = await insertFormValuesInPdf({ + pdf, + formValues, + }); + } + + const { id: documentDataId } = await putNormalizedPdfFileServerSide({ + name: file.name, + type: 'application/pdf', + arrayBuffer: async () => Promise.resolve(pdf), + }); ctx.logger.info({ input: { @@ -48,7 +74,20 @@ export const createDocumentRoute = authenticatedProcedure data: { type: EnvelopeType.DOCUMENT, title, - userTimezone: timezone, + externalId, + visibility, + globalAccessAuth, + globalActionAuth, + recipients: (recipients || []).map((recipient) => ({ + ...recipient, + fields: (recipient.fields || []).map((field) => ({ + ...field, + page: field.pageNumber, + positionX: field.pageX, + positionY: field.pageY, + documentDataId, + })), + })), folderId, envelopeItems: [ { @@ -58,7 +97,10 @@ export const createDocumentRoute = authenticatedProcedure ], }, attachments, - normalizePdf: true, + meta: { + ...meta, + emailSettings: meta?.emailSettings ?? undefined, + }, requestMetadata: ctx.metadata, }); diff --git a/packages/trpc/server/document-router/create-document.types.ts b/packages/trpc/server/document-router/create-document.types.ts index 07ccc8302..1155a17c7 100644 --- a/packages/trpc/server/document-router/create-document.types.ts +++ b/packages/trpc/server/document-router/create-document.types.ts @@ -1,12 +1,27 @@ import { z } from 'zod'; import { zfd } from 'zod-form-data'; -import { ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta'; +import { + ZDocumentAccessAuthTypesSchema, + ZDocumentActionAuthTypesSchema, +} from '@documenso/lib/types/document-auth'; +import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values'; +import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta'; +import { ZDocumentVisibilitySchema } from '@documenso/lib/types/document-visibility'; import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment'; +import { + ZFieldHeightSchema, + ZFieldPageNumberSchema, + ZFieldPageXSchema, + ZFieldPageYSchema, + ZFieldWidthSchema, +} from '@documenso/lib/types/field'; +import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta'; import { zodFormData } from '../../utils/zod-form-data'; +import { ZCreateRecipientSchema } from '../recipient-router/schema'; import type { TrpcRouteMeta } from '../trpc'; -import { ZDocumentTitleSchema } from './schema'; +import { ZDocumentExternalIdSchema, ZDocumentTitleSchema } from './schema'; export const createDocumentMeta: TrpcRouteMeta = { openapi: { @@ -21,8 +36,35 @@ export const createDocumentMeta: TrpcRouteMeta = { export const ZCreateDocumentPayloadSchema = z.object({ title: ZDocumentTitleSchema, - timezone: ZDocumentMetaTimezoneSchema.optional(), - folderId: z.string().describe('The ID of the folder to create the document in').optional(), + externalId: ZDocumentExternalIdSchema.optional(), + visibility: ZDocumentVisibilitySchema.optional(), + globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(), + globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(), + formValues: ZDocumentFormValuesSchema.optional(), + folderId: z + .string() + .describe( + 'The ID of the folder to create the document in. If not provided, the document will be created in the root folder.', + ) + .optional(), + recipients: z + .array( + ZCreateRecipientSchema.extend({ + fields: ZFieldAndMetaSchema.and( + z.object({ + pageNumber: ZFieldPageNumberSchema, + pageX: ZFieldPageXSchema, + pageY: ZFieldPageYSchema, + width: ZFieldWidthSchema, + height: ZFieldHeightSchema, + }), + ) + .array() + .optional(), + }), + ) + + .optional(), attachments: z .array( z.object({ @@ -32,6 +74,7 @@ export const ZCreateDocumentPayloadSchema = z.object({ }), ) .optional(), + meta: ZDocumentMetaCreateSchema.optional(), }); export const ZCreateDocumentRequestSchema = zodFormData({ diff --git a/packages/trpc/server/envelope-router/create-envelope.ts b/packages/trpc/server/envelope-router/create-envelope.ts index 74b98fd42..dfb7927f3 100644 --- a/packages/trpc/server/envelope-router/create-envelope.ts +++ b/packages/trpc/server/envelope-router/create-envelope.ts @@ -3,6 +3,7 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server'; +import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf'; import { authenticatedProcedure } from '../trpc'; import { ZCreateEnvelopeRequestSchema, @@ -58,10 +59,31 @@ export const createEnvelopeRoute = authenticatedProcedure }); } + if (files.some((file) => !file.type.startsWith('application/pdf'))) { + throw new AppError('INVALID_DOCUMENT_FILE', { + message: 'You cannot upload non-PDF files', + statusCode: 400, + }); + } + // 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); + let pdf = Buffer.from(await file.arrayBuffer()); + + if (formValues) { + // eslint-disable-next-line require-atomic-updates + pdf = await insertFormValuesInPdf({ + pdf, + formValues, + }); + } + + const { id: documentDataId } = await putNormalizedPdfFileServerSide({ + name: file.name, + type: 'application/pdf', + arrayBuffer: async () => Promise.resolve(pdf), + }); return { title: file.name,