diff --git a/apps/remix/app/components/embed/embed-direct-template-client-page.tsx b/apps/remix/app/components/embed/embed-direct-template-client-page.tsx index 47c473012..59a9c0312 100644 --- a/apps/remix/app/components/embed/embed-direct-template-client-page.tsx +++ b/apps/remix/app/components/embed/embed-direct-template-client-page.tsx @@ -37,6 +37,7 @@ import { ZDirectTemplateEmbedDataSchema } from '~/types/embed-direct-template-sc import { injectCss } from '~/utils/css-vars'; import type { DirectTemplateLocalField } from '../general/direct-template/direct-template-signing-form'; +import { DocumentSigningAttachmentsPopover } from '../general/document-signing/document-signing-attachments-popover'; import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider'; import { EmbedClientLoading } from './embed-client-loading'; import { EmbedDocumentCompleted } from './embed-document-completed'; @@ -44,6 +45,7 @@ import { EmbedDocumentFields } from './embed-document-fields'; export type EmbedDirectTemplateClientPageProps = { token: string; + envelopeId: string; updatedAt: Date; documentData: DocumentData; recipient: Recipient; @@ -55,9 +57,10 @@ export type EmbedDirectTemplateClientPageProps = { export const EmbedDirectTemplateClientPage = ({ token, + envelopeId, updatedAt, documentData, - recipient: _recipient, + recipient, fields, metadata, hidePoweredBy = false, @@ -321,9 +324,13 @@ export const EmbedDirectTemplateClientPage = ({ } return ( -
+
{(!hasFinishedInit || !hasDocumentLoaded) && } +
+ +
+
{/* Viewer */}
diff --git a/apps/remix/app/components/embed/embed-document-signing-page.tsx b/apps/remix/app/components/embed/embed-document-signing-page.tsx index e795d910e..329a38446 100644 --- a/apps/remix/app/components/embed/embed-document-signing-page.tsx +++ b/apps/remix/app/components/embed/embed-document-signing-page.tsx @@ -37,6 +37,7 @@ import { BrandingLogo } from '~/components/general/branding-logo'; import { injectCss } from '~/utils/css-vars'; import { ZSignDocumentEmbedDataSchema } from '../../types/embed-document-sign-schema'; +import { DocumentSigningAttachmentsPopover } from '../general/document-signing/document-signing-attachments-popover'; import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider'; import { DocumentSigningRecipientProvider } from '../general/document-signing/document-signing-recipient-provider'; import { DocumentSigningRejectDialog } from '../general/document-signing/document-signing-reject-dialog'; @@ -48,6 +49,7 @@ import { EmbedDocumentRejected } from './embed-document-rejected'; export type EmbedSignDocumentClientPageProps = { token: string; documentId: number; + envelopeId: string; documentData: DocumentData; recipient: RecipientWithFields; fields: Field[]; @@ -62,6 +64,7 @@ export type EmbedSignDocumentClientPageProps = { export const EmbedSignDocumentClientPage = ({ token, documentId, + envelopeId, documentData, recipient, fields, @@ -274,15 +277,17 @@ export const EmbedSignDocumentClientPage = ({
{(!hasFinishedInit || !hasDocumentLoaded) && } - {allowDocumentRejection && ( -
+
+ + + {allowDocumentRejection && ( -
- )} + )} +
{/* Viewer */} diff --git a/apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx b/apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx new file mode 100644 index 000000000..7aba97846 --- /dev/null +++ b/apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx @@ -0,0 +1,79 @@ +import { Trans } from '@lingui/react/macro'; +import { ExternalLink, PaperclipIcon } from 'lucide-react'; + +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; + +export type DocumentSigningAttachmentsPopoverProps = { + envelopeId: string; + token: string; +}; + +export const DocumentSigningAttachmentsPopover = ({ + envelopeId, + token, +}: DocumentSigningAttachmentsPopoverProps) => { + const { data: attachments } = trpc.envelope.attachment.find.useQuery({ + envelopeId, + token, + }); + + if (!attachments || attachments.data.length === 0) { + return null; + } + + return ( + + + + + + +
+
+

+ Attachments +

+

+ Documents and resources related to this envelope. +

+
+ +
+ {attachments?.data.map((attachment) => ( + +
+
+ +
+ + + {attachment.label} + +
+ + +
+ ))} +
+
+
+
+ ); +}; diff --git a/apps/remix/app/components/general/document-signing/document-signing-page-view-v1.tsx b/apps/remix/app/components/general/document-signing/document-signing-page-view-v1.tsx index 02755ea44..b3ee5727e 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-page-view-v1.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-page-view-v1.tsx @@ -32,6 +32,7 @@ import { Card, CardContent } from '@documenso/ui/primitives/card'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer'; +import { DocumentSigningAttachmentsPopover } from '~/components/general/document-signing/document-signing-attachments-popover'; import { DocumentSigningAutoSign } from '~/components/general/document-signing/document-signing-auto-sign'; import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field'; import { DocumentSigningDateField } from '~/components/general/document-signing/document-signing-date-field'; @@ -231,7 +232,13 @@ export const DocumentSigningPageViewV1 = ({
- +
+ + +
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 6d334994b..aa840b32a 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 @@ -19,6 +19,7 @@ import { SignFieldNumberDialog } from '~/components/dialogs/sign-field-number-di import { SignFieldSignatureDialog } from '~/components/dialogs/sign-field-signature-dialog'; import { SignFieldTextDialog } from '~/components/dialogs/sign-field-text-dialog'; +import { DocumentSigningAttachmentsPopover } from '../document-signing/document-signing-attachments-popover'; import { EnvelopeItemSelector } from '../envelope-editor/envelope-file-selector'; import EnvelopeSignerForm from '../envelope-signing/envelope-signer-form'; import { EnvelopeSignerHeader } from '../envelope-signing/envelope-signer-header'; @@ -31,8 +32,13 @@ const EnvelopeSignerPageRenderer = lazy( export const DocumentSigningPageViewV2 = () => { const { envelopeItems, currentEnvelopeItem, setCurrentEnvelopeItem } = useCurrentEnvelopeRender(); - const { envelope, recipientFields, recipientFieldsRemaining, showPendingFieldTooltip } = - useRequiredEnvelopeSigningContext(); + const { + envelope, + recipient, + recipientFields, + recipientFieldsRemaining, + showPendingFieldTooltip, + } = useRequiredEnvelopeSigningContext(); return (
@@ -83,6 +89,10 @@ export const DocumentSigningPageViewV2 = () => { Actions +
+ +
+ {/* Todo: Allow selecting which document to download and/or the original */} + + + +
+
+

+ Attachments +

+

+ Add links to relevant documents or resources. +

+
+ + {attachments && attachments.data.length > 0 && ( +
+ {attachments?.data.map((attachment) => ( +
+
+

{attachment.label}

+ + {attachment.data} + +
+ + +
+ ))} +
+ )} + + {!isAdding && ( + + )} + + {isAdding && ( +
+ + ( + + + + + + + )} + /> + + ( + + + + + + + )} + /> + +
+ + +
+ + + )} +
+
+ + ); +}; diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx index 859619584..9b0b51fe4 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx @@ -22,6 +22,7 @@ import { EnvelopeDistributeDialog } from '~/components/dialogs/envelope-distribu import { EnvelopeRedistributeDialog } from '~/components/dialogs/envelope-redistribute-dialog'; import { TemplateUseDialog } from '~/components/dialogs/template-use-dialog'; import { BrandingLogo } from '~/components/general/branding-logo'; +import { DocumentAttachmentsPopover } from '~/components/general/document/document-attachments-popover'; import { EnvelopeEditorSettingsDialog } from '~/components/general/envelope-editor/envelope-editor-settings-dialog'; import { useCurrentTeam } from '~/providers/team'; @@ -131,6 +132,8 @@ export default function EnvelopeEditorHeader() {
+ + diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.legacy_editor.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.legacy_editor.tsx index 670db608e..79a8fd1f8 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.legacy_editor.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.legacy_editor.tsx @@ -9,6 +9,7 @@ import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { logDocumentAccess } from '@documenso/lib/utils/logger'; import { canAccessTeamDocument, formatDocumentsPath } from '@documenso/lib/utils/teams'; +import { DocumentAttachmentsPopover } from '~/components/general/document/document-attachments-popover'; import { DocumentEditForm } from '~/components/general/document/document-edit-form'; import { DocumentStatus } from '~/components/general/document/document-status'; import { LegacyFieldWarningPopover } from '~/components/general/legacy-field-warning-popover'; @@ -122,11 +123,13 @@ export default function DocumentEditPage() {
- {document.useLegacyFieldInsertion && ( -
+
+ + + {document.useLegacyFieldInsertion && ( -
- )} + )} +
+ + ; @@ -262,6 +272,15 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({ }) .optional(), formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(), + attachments: z + .array( + z.object({ + label: z.string().min(1, 'Label is required'), + data: z.string().url('Must be a valid URL'), + type: ZEnvelopeAttachmentTypeSchema.optional().default('link'), + }), + ) + .optional(), }); export type TCreateDocumentFromTemplateMutationSchema = z.infer< diff --git a/packages/lib/server-only/envelope-attachment/create-attachment.ts b/packages/lib/server-only/envelope-attachment/create-attachment.ts new file mode 100644 index 000000000..f55b7f56d --- /dev/null +++ b/packages/lib/server-only/envelope-attachment/create-attachment.ts @@ -0,0 +1,50 @@ +import { DocumentStatus } from '@prisma/client'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { prisma } from '@documenso/prisma'; + +import { buildTeamWhereQuery } from '../../utils/teams'; + +export type CreateAttachmentOptions = { + envelopeId: string; + teamId: number; + userId: number; + data: { + label: string; + data: string; + }; +}; + +export const createAttachment = async ({ + envelopeId, + teamId, + userId, + data, +}: CreateAttachmentOptions) => { + const envelope = await prisma.envelope.findFirst({ + where: { + id: envelopeId, + team: buildTeamWhereQuery({ teamId, userId }), + }, + }); + + if (!envelope) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Envelope not found', + }); + } + + if (envelope.status === DocumentStatus.COMPLETED || envelope.status === DocumentStatus.REJECTED) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: 'Attachments can not be modified after the document has been completed or rejected', + }); + } + + return await prisma.envelopeAttachment.create({ + data: { + envelopeId, + type: 'link', + ...data, + }, + }); +}; diff --git a/packages/lib/server-only/envelope-attachment/delete-attachment.ts b/packages/lib/server-only/envelope-attachment/delete-attachment.ts new file mode 100644 index 000000000..3268bf5ba --- /dev/null +++ b/packages/lib/server-only/envelope-attachment/delete-attachment.ts @@ -0,0 +1,47 @@ +import { DocumentStatus } from '@prisma/client'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { prisma } from '@documenso/prisma'; + +import { buildTeamWhereQuery } from '../../utils/teams'; + +export type DeleteAttachmentOptions = { + id: string; + userId: number; + teamId: number; +}; + +export const deleteAttachment = async ({ id, userId, teamId }: DeleteAttachmentOptions) => { + const attachment = await prisma.envelopeAttachment.findFirst({ + where: { + id, + envelope: { + team: buildTeamWhereQuery({ teamId, userId }), + }, + }, + include: { + envelope: true, + }, + }); + + if (!attachment) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Attachment not found', + }); + } + + if ( + attachment.envelope.status === DocumentStatus.COMPLETED || + attachment.envelope.status === DocumentStatus.REJECTED + ) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: 'Attachments can not be modified after the document has been completed or rejected', + }); + } + + await prisma.envelopeAttachment.delete({ + where: { + id, + }, + }); +}; diff --git a/packages/lib/server-only/envelope-attachment/find-attachments-by-envelope-id.ts b/packages/lib/server-only/envelope-attachment/find-attachments-by-envelope-id.ts new file mode 100644 index 000000000..ff6402fea --- /dev/null +++ b/packages/lib/server-only/envelope-attachment/find-attachments-by-envelope-id.ts @@ -0,0 +1,38 @@ +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { prisma } from '@documenso/prisma'; + +import { buildTeamWhereQuery } from '../../utils/teams'; + +export type FindAttachmentsByEnvelopeIdOptions = { + envelopeId: string; + userId: number; + teamId: number; +}; + +export const findAttachmentsByEnvelopeId = async ({ + envelopeId, + userId, + teamId, +}: FindAttachmentsByEnvelopeIdOptions) => { + const envelope = await prisma.envelope.findFirst({ + where: { + id: envelopeId, + team: buildTeamWhereQuery({ teamId, userId }), + }, + }); + + if (!envelope) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Envelope not found', + }); + } + + return await prisma.envelopeAttachment.findMany({ + where: { + envelopeId, + }, + orderBy: { + createdAt: 'asc', + }, + }); +}; diff --git a/packages/lib/server-only/envelope-attachment/find-attachments-by-token.ts b/packages/lib/server-only/envelope-attachment/find-attachments-by-token.ts new file mode 100644 index 000000000..699d0c00a --- /dev/null +++ b/packages/lib/server-only/envelope-attachment/find-attachments-by-token.ts @@ -0,0 +1,70 @@ +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { prisma } from '@documenso/prisma'; + +export type FindAttachmentsByTokenOptions = { + envelopeId: string; + token: string; +}; + +export const findAttachmentsByToken = async ({ + envelopeId, + token, +}: FindAttachmentsByTokenOptions) => { + const envelope = await prisma.envelope.findFirst({ + where: { + id: envelopeId, + recipients: { + some: { + token, + }, + }, + }, + }); + + if (!envelope) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Envelope not found', + }); + } + + return await prisma.envelopeAttachment.findMany({ + where: { + envelopeId, + }, + orderBy: { + createdAt: 'asc', + }, + }); +}; + +export type FindAttachmentsByTeamOptions = { + envelopeId: string; + teamId: number; +}; + +export const findAttachmentsByTeam = async ({ + envelopeId, + teamId, +}: FindAttachmentsByTeamOptions) => { + const envelope = await prisma.envelope.findFirst({ + where: { + id: envelopeId, + teamId, + }, + }); + + if (!envelope) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Envelope not found', + }); + } + + return await prisma.envelopeAttachment.findMany({ + where: { + envelopeId, + }, + orderBy: { + createdAt: 'asc', + }, + }); +}; diff --git a/packages/lib/server-only/envelope-attachment/update-attachment.ts b/packages/lib/server-only/envelope-attachment/update-attachment.ts new file mode 100644 index 000000000..4d1dae350 --- /dev/null +++ b/packages/lib/server-only/envelope-attachment/update-attachment.ts @@ -0,0 +1,49 @@ +import { DocumentStatus } from '@prisma/client'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { prisma } from '@documenso/prisma'; + +import { buildTeamWhereQuery } from '../../utils/teams'; + +export type UpdateAttachmentOptions = { + id: string; + userId: number; + teamId: number; + data: { label?: string; data?: string }; +}; + +export const updateAttachment = async ({ id, teamId, userId, data }: UpdateAttachmentOptions) => { + const attachment = await prisma.envelopeAttachment.findFirst({ + where: { + id, + envelope: { + team: buildTeamWhereQuery({ teamId, userId }), + }, + }, + include: { + envelope: true, + }, + }); + + if (!attachment) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Attachment not found', + }); + } + + if ( + attachment.envelope.status === DocumentStatus.COMPLETED || + attachment.envelope.status === DocumentStatus.REJECTED + ) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: 'Attachments can not be modified after the document has been completed or rejected', + }); + } + + return await prisma.envelopeAttachment.update({ + where: { + id, + }, + data, + }); +}; diff --git a/packages/lib/server-only/envelope/create-envelope.ts b/packages/lib/server-only/envelope/create-envelope.ts index 2a0bc9fee..5f4198377 100644 --- a/packages/lib/server-only/envelope/create-envelope.ts +++ b/packages/lib/server-only/envelope/create-envelope.ts @@ -20,6 +20,7 @@ import type { TCreateEnvelopeRequest } from '@documenso/trpc/server/envelope-rou import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth'; import type { TDocumentFormValues } from '../../types/document-form-values'; +import type { TEnvelopeAttachmentType } from '../../types/envelope-attachment'; import { ZWebhookDocumentSchema, mapEnvelopeToWebhookDocumentPayload, @@ -58,6 +59,11 @@ export type CreateEnvelopeOptions = { recipients?: TCreateEnvelopeRequest['recipients']; folderId?: string; }; + attachments?: Array<{ + label: string; + data: string; + type?: TEnvelopeAttachmentType; + }>; meta?: Partial>; requestMetadata: ApiRequestMetadata; }; @@ -67,6 +73,7 @@ export const createEnvelope = async ({ teamId, normalizePdf, data, + attachments, meta, requestMetadata, internalVersion, @@ -246,6 +253,15 @@ export const createEnvelope = async ({ })), }, }, + envelopeAttachments: { + createMany: { + data: (attachments || []).map((attachment) => ({ + label: attachment.label, + data: attachment.data, + type: attachment.type ?? 'link', + })), + }, + }, userId, teamId, authOptions, @@ -338,6 +354,7 @@ export const createEnvelope = async ({ fields: true, folder: true, envelopeItems: true, + envelopeAttachments: true, }, }); diff --git a/packages/lib/server-only/template/create-document-from-direct-template.ts b/packages/lib/server-only/template/create-document-from-direct-template.ts index fb354d8db..f8005ca51 100644 --- a/packages/lib/server-only/template/create-document-from-direct-template.ts +++ b/packages/lib/server-only/template/create-document-from-direct-template.ts @@ -640,6 +640,23 @@ export const createDocumentFromDirectTemplate = async ({ data: auditLogsToCreate, }); + const templateAttachments = await tx.envelopeAttachment.findMany({ + where: { + envelopeId: directTemplateEnvelope.id, + }, + }); + + if (templateAttachments.length > 0) { + await tx.envelopeAttachment.createMany({ + data: templateAttachments.map((attachment) => ({ + envelopeId: createdEnvelope.id, + type: attachment.type, + label: attachment.label, + data: attachment.data, + })), + }); + } + // Send email to template owner. const emailTemplate = createElement(DocumentCreatedFromDirectTemplateEmailTemplate, { recipientName: directRecipientEmail, diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts index 3aa48e29a..7c3e63d48 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -91,6 +91,12 @@ export type CreateDocumentFromTemplateOptions = { envelopeItemId?: string; }[]; + attachments?: Array<{ + label: string; + data: string; + type?: 'link'; + }>; + /** * Values that will override the predefined values in the template. */ @@ -295,6 +301,7 @@ export const createDocumentFromTemplate = async ({ requestMetadata, folderId, prefillFields, + attachments, }: CreateDocumentFromTemplateOptions) => { const { envelopeWhereInput } = await getEnvelopeWhereInput({ id, @@ -667,6 +674,33 @@ export const createDocumentFromTemplate = async ({ }), }); + const templateAttachments = await tx.envelopeAttachment.findMany({ + where: { + envelopeId: template.id, + }, + }); + + const attachmentsToCreate = [ + ...templateAttachments.map((attachment) => ({ + envelopeId: envelope.id, + type: attachment.type, + label: attachment.label, + data: attachment.data, + })), + ...(attachments || []).map((attachment) => ({ + envelopeId: envelope.id, + type: attachment.type || 'link', + label: attachment.label, + data: attachment.data, + })), + ]; + + if (attachmentsToCreate.length > 0) { + await tx.envelopeAttachment.createMany({ + data: attachmentsToCreate, + }); + } + const createdEnvelope = await tx.envelope.findFirst({ where: { id: envelope.id, diff --git a/packages/lib/types/envelope-attachment.ts b/packages/lib/types/envelope-attachment.ts new file mode 100644 index 000000000..3634a8ab7 --- /dev/null +++ b/packages/lib/types/envelope-attachment.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; + +export const ZEnvelopeAttachmentTypeSchema = z.enum(['link']); + +export type TEnvelopeAttachmentType = z.infer; diff --git a/packages/prisma/migrations/20251023021213_add_envelope_attachments/migration.sql b/packages/prisma/migrations/20251023021213_add_envelope_attachments/migration.sql new file mode 100644 index 000000000..7540b41c9 --- /dev/null +++ b/packages/prisma/migrations/20251023021213_add_envelope_attachments/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "EnvelopeAttachment" ( + "id" TEXT NOT NULL, + "type" TEXT NOT NULL, + "label" TEXT NOT NULL, + "data" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "envelopeId" TEXT NOT NULL, + + CONSTRAINT "EnvelopeAttachment_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "EnvelopeAttachment" ADD CONSTRAINT "EnvelopeAttachment_envelopeId_fkey" FOREIGN KEY ("envelopeId") REFERENCES "Envelope"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 10e68cade..0db892527 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -422,6 +422,8 @@ model Envelope { documentMetaId String @unique documentMeta DocumentMeta @relation(fields: [documentMetaId], references: [id]) + + envelopeAttachments EnvelopeAttachment[] } model EnvelopeItem { @@ -508,6 +510,22 @@ model DocumentMeta { envelope Envelope? } +/// @zod.import(["import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';"]) +model EnvelopeAttachment { + id String @id @default(cuid()) + + type String /// [EnvelopeAttachmentType] @zod.custom.use(ZEnvelopeAttachmentTypeSchema) + label String + + data String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + envelopeId String + envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade) +} + enum ReadStatus { NOT_OPENED OPENED diff --git a/packages/prisma/types/types.d.ts b/packages/prisma/types/types.d.ts index 2621bd54a..d8c0f8103 100644 --- a/packages/prisma/types/types.d.ts +++ b/packages/prisma/types/types.d.ts @@ -5,6 +5,7 @@ import type { } from '@documenso/lib/types/document-auth'; import type { TDocumentEmailSettings } from '@documenso/lib/types/document-email'; import type { TDocumentFormValues } from '@documenso/lib/types/document-form-values'; +import type { TEnvelopeAttachmentType } from '@documenso/lib/types/envelope-attachment'; import type { TFieldMetaNotOptionalSchema } from '@documenso/lib/types/field-meta'; import type { TClaimFlags } from '@documenso/lib/types/subscription'; @@ -23,6 +24,8 @@ declare global { type RecipientAuthOptions = TRecipientAuthOptions; type FieldMeta = TFieldMetaNotOptionalSchema; + + type EnvelopeAttachmentType = TEnvelopeAttachmentType; } } diff --git a/packages/trpc/server/document-router/attachment/create-attachment.ts b/packages/trpc/server/document-router/attachment/create-attachment.ts new file mode 100644 index 000000000..16eea3121 --- /dev/null +++ b/packages/trpc/server/document-router/attachment/create-attachment.ts @@ -0,0 +1,50 @@ +import { EnvelopeType } from '@prisma/client'; + +import { createAttachment } from '@documenso/lib/server-only/envelope-attachment/create-attachment'; +import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id'; + +import { authenticatedProcedure } from '../../trpc'; +import { + ZCreateAttachmentRequestSchema, + ZCreateAttachmentResponseSchema, +} from './create-attachment.types'; + +export const createAttachmentRoute = authenticatedProcedure + .meta({ + openapi: { + method: 'POST', + path: '/document/attachment/create', + summary: 'Create attachment', + description: 'Create a new attachment for a document', + tags: ['Document'], + }, + }) + .input(ZCreateAttachmentRequestSchema) + .output(ZCreateAttachmentResponseSchema) + .mutation(async ({ input, ctx }) => { + const { teamId } = ctx; + const userId = ctx.user.id; + + const { documentId, data } = input; + + ctx.logger.info({ + input: { documentId, label: data.label }, + }); + + const envelope = await getEnvelopeById({ + id: { + type: 'documentId', + id: documentId, + }, + userId, + teamId, + type: EnvelopeType.DOCUMENT, + }); + + await createAttachment({ + envelopeId: envelope.id, + teamId, + userId, + data, + }); + }); diff --git a/packages/trpc/server/document-router/attachment/create-attachment.types.ts b/packages/trpc/server/document-router/attachment/create-attachment.types.ts new file mode 100644 index 000000000..1baf69b24 --- /dev/null +++ b/packages/trpc/server/document-router/attachment/create-attachment.types.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +export const ZCreateAttachmentRequestSchema = z.object({ + documentId: z.number(), + data: z.object({ + label: z.string().min(1, 'Label is required'), + data: z.string().url('Must be a valid URL'), + }), +}); + +export const ZCreateAttachmentResponseSchema = z.void(); + +export type TCreateAttachmentRequest = z.infer; +export type TCreateAttachmentResponse = z.infer; diff --git a/packages/trpc/server/document-router/attachment/delete-attachment.ts b/packages/trpc/server/document-router/attachment/delete-attachment.ts new file mode 100644 index 000000000..f7c058c5b --- /dev/null +++ b/packages/trpc/server/document-router/attachment/delete-attachment.ts @@ -0,0 +1,36 @@ +import { deleteAttachment } from '@documenso/lib/server-only/envelope-attachment/delete-attachment'; + +import { authenticatedProcedure } from '../../trpc'; +import { + ZDeleteAttachmentRequestSchema, + ZDeleteAttachmentResponseSchema, +} from './delete-attachment.types'; + +export const deleteAttachmentRoute = authenticatedProcedure + .meta({ + openapi: { + method: 'POST', + path: '/document/attachment/delete', + summary: 'Delete attachment', + description: 'Delete an attachment from a document', + tags: ['Document'], + }, + }) + .input(ZDeleteAttachmentRequestSchema) + .output(ZDeleteAttachmentResponseSchema) + .mutation(async ({ input, ctx }) => { + const { teamId } = ctx; + const userId = ctx.user.id; + + const { id } = input; + + ctx.logger.info({ + input: { id }, + }); + + await deleteAttachment({ + id, + userId, + teamId, + }); + }); diff --git a/packages/trpc/server/document-router/attachment/delete-attachment.types.ts b/packages/trpc/server/document-router/attachment/delete-attachment.types.ts new file mode 100644 index 000000000..81d7c0614 --- /dev/null +++ b/packages/trpc/server/document-router/attachment/delete-attachment.types.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const ZDeleteAttachmentRequestSchema = z.object({ + id: z.string(), +}); + +export const ZDeleteAttachmentResponseSchema = z.void(); + +export type TDeleteAttachmentRequest = z.infer; +export type TDeleteAttachmentResponse = z.infer; diff --git a/packages/trpc/server/document-router/attachment/find-attachments.ts b/packages/trpc/server/document-router/attachment/find-attachments.ts new file mode 100644 index 000000000..a90718363 --- /dev/null +++ b/packages/trpc/server/document-router/attachment/find-attachments.ts @@ -0,0 +1,52 @@ +import { EnvelopeType } from '@prisma/client'; + +import { findAttachmentsByEnvelopeId } from '@documenso/lib/server-only/envelope-attachment/find-attachments-by-envelope-id'; +import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id'; + +import { authenticatedProcedure } from '../../trpc'; +import { + ZFindAttachmentsRequestSchema, + ZFindAttachmentsResponseSchema, +} from './find-attachments.types'; + +export const findAttachmentsRoute = authenticatedProcedure + .meta({ + openapi: { + method: 'GET', + path: '/document/attachment', + summary: 'Find attachments', + description: 'Find all attachments for a document', + tags: ['Document'], + }, + }) + .input(ZFindAttachmentsRequestSchema) + .output(ZFindAttachmentsResponseSchema) + .query(async ({ input, ctx }) => { + const { documentId } = input; + const { teamId } = ctx; + const userId = ctx.user.id; + + ctx.logger.info({ + input: { documentId }, + }); + + const envelope = await getEnvelopeById({ + id: { + type: 'documentId', + id: documentId, + }, + userId, + teamId, + type: EnvelopeType.DOCUMENT, + }); + + const data = await findAttachmentsByEnvelopeId({ + envelopeId: envelope.id, + teamId, + userId, + }); + + return { + data, + }; + }); diff --git a/packages/trpc/server/document-router/attachment/find-attachments.types.ts b/packages/trpc/server/document-router/attachment/find-attachments.types.ts new file mode 100644 index 000000000..7f73f6210 --- /dev/null +++ b/packages/trpc/server/document-router/attachment/find-attachments.types.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment'; + +export const ZFindAttachmentsRequestSchema = z.object({ + documentId: z.number(), +}); + +export const ZFindAttachmentsResponseSchema = z.object({ + data: z.array( + z.object({ + id: z.string(), + type: ZEnvelopeAttachmentTypeSchema, + label: z.string(), + data: z.string(), + }), + ), +}); + +export type TFindAttachmentsRequest = z.infer; +export type TFindAttachmentsResponse = z.infer; diff --git a/packages/trpc/server/document-router/attachment/update-attachment.ts b/packages/trpc/server/document-router/attachment/update-attachment.ts new file mode 100644 index 000000000..d03715d03 --- /dev/null +++ b/packages/trpc/server/document-router/attachment/update-attachment.ts @@ -0,0 +1,37 @@ +import { updateAttachment } from '@documenso/lib/server-only/envelope-attachment/update-attachment'; + +import { authenticatedProcedure } from '../../trpc'; +import { + ZUpdateAttachmentRequestSchema, + ZUpdateAttachmentResponseSchema, +} from './update-attachment.types'; + +export const updateAttachmentRoute = authenticatedProcedure + .meta({ + openapi: { + method: 'POST', + path: '/document/attachment/update', + summary: 'Update attachment', + description: 'Update an existing attachment', + tags: ['Document'], + }, + }) + .input(ZUpdateAttachmentRequestSchema) + .output(ZUpdateAttachmentResponseSchema) + .mutation(async ({ input, ctx }) => { + const { teamId } = ctx; + const userId = ctx.user.id; + + const { id, data } = input; + + ctx.logger.info({ + input: { id }, + }); + + await updateAttachment({ + id, + userId, + teamId, + data, + }); + }); diff --git a/packages/trpc/server/document-router/attachment/update-attachment.types.ts b/packages/trpc/server/document-router/attachment/update-attachment.types.ts new file mode 100644 index 000000000..eaf12b559 --- /dev/null +++ b/packages/trpc/server/document-router/attachment/update-attachment.types.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +export const ZUpdateAttachmentRequestSchema = z.object({ + id: z.string(), + data: z.object({ + label: z.string().min(1, 'Label is required'), + data: z.string().url('Must be a valid URL'), + }), +}); + +export const ZUpdateAttachmentResponseSchema = z.void(); + +export type TUpdateAttachmentRequest = z.infer; +export type TUpdateAttachmentResponse = z.infer; diff --git a/packages/trpc/server/document-router/create-document-temporary.ts b/packages/trpc/server/document-router/create-document-temporary.ts index 878da25c7..2159b0a65 100644 --- a/packages/trpc/server/document-router/create-document-temporary.ts +++ b/packages/trpc/server/document-router/create-document-temporary.ts @@ -37,6 +37,7 @@ export const createDocumentTemporaryRoute = authenticatedProcedure recipients, meta, folderId, + attachments, } = input; const { remaining } = await getServerLimits({ userId: user.id, teamId }); @@ -86,6 +87,7 @@ export const createDocumentTemporaryRoute = authenticatedProcedure }, ], }, + attachments, meta: { ...meta, emailSettings: meta?.emailSettings ?? undefined, diff --git a/packages/trpc/server/document-router/create-document-temporary.types.ts b/packages/trpc/server/document-router/create-document-temporary.types.ts index 5223adbfb..8895e1a99 100644 --- a/packages/trpc/server/document-router/create-document-temporary.types.ts +++ b/packages/trpc/server/document-router/create-document-temporary.types.ts @@ -7,6 +7,7 @@ import { } from '@documenso/lib/types/document-auth'; import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values'; import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta'; +import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment'; import { ZFieldHeightSchema, ZFieldPageNumberSchema, @@ -68,6 +69,15 @@ export const ZCreateDocumentTemporaryRequestSchema = z.object({ }), ) + .optional(), + attachments: z + .array( + z.object({ + label: z.string().min(1, 'Label is required'), + data: z.string().url('Must be a valid URL'), + type: ZEnvelopeAttachmentTypeSchema.optional().default('link'), + }), + ) .optional(), meta: ZDocumentMetaCreateSchema.optional(), }); diff --git a/packages/trpc/server/document-router/create-document.ts b/packages/trpc/server/document-router/create-document.ts index 71e1b2594..275978df9 100644 --- a/packages/trpc/server/document-router/create-document.ts +++ b/packages/trpc/server/document-router/create-document.ts @@ -16,7 +16,7 @@ export const createDocumentRoute = authenticatedProcedure .output(ZCreateDocumentResponseSchema) .mutation(async ({ input, ctx }) => { const { user, teamId } = ctx; - const { title, documentDataId, timezone, folderId } = input; + const { title, documentDataId, timezone, folderId, attachments } = input; ctx.logger.info({ input: { @@ -48,6 +48,7 @@ export const createDocumentRoute = authenticatedProcedure }, ], }, + attachments, normalizePdf: true, 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 7c42e9286..43fa32291 100644 --- a/packages/trpc/server/document-router/create-document.types.ts +++ b/packages/trpc/server/document-router/create-document.types.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta'; +import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment'; import { ZDocumentTitleSchema } from './schema'; @@ -19,6 +20,15 @@ export const ZCreateDocumentRequestSchema = z.object({ documentDataId: z.string().min(1), timezone: ZDocumentMetaTimezoneSchema.optional(), folderId: z.string().describe('The ID of the folder to create the document in').optional(), + attachments: z + .array( + z.object({ + label: z.string().min(1, 'Label is required'), + data: z.string().url('Must be a valid URL'), + type: ZEnvelopeAttachmentTypeSchema.optional().default('link'), + }), + ) + .optional(), }); export const ZCreateDocumentResponseSchema = z.object({ diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index d49037f8c..f5d34a141 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -1,5 +1,9 @@ import { router } from '../trpc'; import { accessAuthRequest2FAEmailRoute } from './access-auth-request-2fa-email'; +import { createAttachmentRoute } from './attachment/create-attachment'; +import { deleteAttachmentRoute } from './attachment/delete-attachment'; +import { findAttachmentsRoute } from './attachment/find-attachments'; +import { updateAttachmentRoute } from './attachment/update-attachment'; import { createDocumentRoute } from './create-document'; import { createDocumentTemporaryRoute } from './create-document-temporary'; import { deleteDocumentRoute } from './delete-document'; @@ -53,4 +57,10 @@ export const documentRouter = router({ find: findInboxRoute, getCount: getInboxCountRoute, }), + attachment: { + create: createAttachmentRoute, + update: updateAttachmentRoute, + delete: deleteAttachmentRoute, + find: findAttachmentsRoute, + }, }); diff --git a/packages/trpc/server/envelope-router/attachment/create-attachment.ts b/packages/trpc/server/envelope-router/attachment/create-attachment.ts new file mode 100644 index 000000000..239052005 --- /dev/null +++ b/packages/trpc/server/envelope-router/attachment/create-attachment.ts @@ -0,0 +1,37 @@ +import { createAttachment } from '@documenso/lib/server-only/envelope-attachment/create-attachment'; + +import { authenticatedProcedure } from '../../trpc'; +import { + ZCreateAttachmentRequestSchema, + ZCreateAttachmentResponseSchema, +} from './create-attachment.types'; + +export const createAttachmentRoute = authenticatedProcedure + .meta({ + openapi: { + method: 'POST', + path: '/envelope/attachment/create', + summary: 'Create attachment', + description: 'Create a new attachment for an envelope', + tags: ['Envelope'], + }, + }) + .input(ZCreateAttachmentRequestSchema) + .output(ZCreateAttachmentResponseSchema) + .mutation(async ({ input, ctx }) => { + const { teamId } = ctx; + const userId = ctx.user.id; + + const { envelopeId, data } = input; + + ctx.logger.info({ + input: { envelopeId, label: data.label }, + }); + + await createAttachment({ + envelopeId, + teamId, + userId, + data, + }); + }); diff --git a/packages/trpc/server/envelope-router/attachment/create-attachment.types.ts b/packages/trpc/server/envelope-router/attachment/create-attachment.types.ts new file mode 100644 index 000000000..106a6e751 --- /dev/null +++ b/packages/trpc/server/envelope-router/attachment/create-attachment.types.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +export const ZCreateAttachmentRequestSchema = z.object({ + envelopeId: z.string(), + data: z.object({ + label: z.string().min(1, 'Label is required'), + data: z.string().url('Must be a valid URL'), + }), +}); + +export const ZCreateAttachmentResponseSchema = z.void(); + +export type TCreateAttachmentRequest = z.infer; +export type TCreateAttachmentResponse = z.infer; diff --git a/packages/trpc/server/envelope-router/attachment/delete-attachment.ts b/packages/trpc/server/envelope-router/attachment/delete-attachment.ts new file mode 100644 index 000000000..3fd82805e --- /dev/null +++ b/packages/trpc/server/envelope-router/attachment/delete-attachment.ts @@ -0,0 +1,36 @@ +import { deleteAttachment } from '@documenso/lib/server-only/envelope-attachment/delete-attachment'; + +import { authenticatedProcedure } from '../../trpc'; +import { + ZDeleteAttachmentRequestSchema, + ZDeleteAttachmentResponseSchema, +} from './delete-attachment.types'; + +export const deleteAttachmentRoute = authenticatedProcedure + .meta({ + openapi: { + method: 'POST', + path: '/envelope/attachment/delete', + summary: 'Delete attachment', + description: 'Delete an attachment from an envelope', + tags: ['Envelope'], + }, + }) + .input(ZDeleteAttachmentRequestSchema) + .output(ZDeleteAttachmentResponseSchema) + .mutation(async ({ input, ctx }) => { + const { teamId } = ctx; + const userId = ctx.user.id; + + const { id } = input; + + ctx.logger.info({ + input: { id }, + }); + + await deleteAttachment({ + id, + userId, + teamId, + }); + }); diff --git a/packages/trpc/server/envelope-router/attachment/delete-attachment.types.ts b/packages/trpc/server/envelope-router/attachment/delete-attachment.types.ts new file mode 100644 index 000000000..81d7c0614 --- /dev/null +++ b/packages/trpc/server/envelope-router/attachment/delete-attachment.types.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const ZDeleteAttachmentRequestSchema = z.object({ + id: z.string(), +}); + +export const ZDeleteAttachmentResponseSchema = z.void(); + +export type TDeleteAttachmentRequest = z.infer; +export type TDeleteAttachmentResponse = z.infer; diff --git a/packages/trpc/server/envelope-router/attachment/find-attachments.ts b/packages/trpc/server/envelope-router/attachment/find-attachments.ts new file mode 100644 index 000000000..74e81c5e1 --- /dev/null +++ b/packages/trpc/server/envelope-router/attachment/find-attachments.ts @@ -0,0 +1,52 @@ +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { findAttachmentsByEnvelopeId } from '@documenso/lib/server-only/envelope-attachment/find-attachments-by-envelope-id'; +import { findAttachmentsByToken } from '@documenso/lib/server-only/envelope-attachment/find-attachments-by-token'; + +import { procedure } from '../../trpc'; +import { + ZFindAttachmentsRequestSchema, + ZFindAttachmentsResponseSchema, +} from './find-attachments.types'; + +export const findAttachmentsRoute = procedure + .meta({ + openapi: { + method: 'GET', + path: '/envelope/attachment', + summary: 'Find attachments', + description: 'Find all attachments for an envelope', + tags: ['Envelope'], + }, + }) + .input(ZFindAttachmentsRequestSchema) + .output(ZFindAttachmentsResponseSchema) + .query(async ({ input, ctx }) => { + const { envelopeId, token } = input; + + ctx.logger.info({ + input: { envelopeId }, + }); + + if (token) { + const data = await findAttachmentsByToken({ envelopeId, token }); + + return { + data, + }; + } + + const { teamId } = ctx; + const userId = ctx.user?.id; + + if (!userId || !teamId) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'You must be authenticated to access this resource', + }); + } + + const data = await findAttachmentsByEnvelopeId({ envelopeId, teamId, userId }); + + return { + data, + }; + }); diff --git a/packages/trpc/server/envelope-router/attachment/find-attachments.types.ts b/packages/trpc/server/envelope-router/attachment/find-attachments.types.ts new file mode 100644 index 000000000..e61b2d2cb --- /dev/null +++ b/packages/trpc/server/envelope-router/attachment/find-attachments.types.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; + +import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment'; + +export const ZFindAttachmentsRequestSchema = z.object({ + envelopeId: z.string(), + token: z.string().optional(), +}); + +export const ZFindAttachmentsResponseSchema = z.object({ + data: z.array( + z.object({ + id: z.string(), + type: ZEnvelopeAttachmentTypeSchema, + label: z.string(), + data: z.string(), + }), + ), +}); + +export type TFindAttachmentsRequest = z.infer; +export type TFindAttachmentsResponse = z.infer; diff --git a/packages/trpc/server/envelope-router/attachment/update-attachment.ts b/packages/trpc/server/envelope-router/attachment/update-attachment.ts new file mode 100644 index 000000000..11a7dfa5b --- /dev/null +++ b/packages/trpc/server/envelope-router/attachment/update-attachment.ts @@ -0,0 +1,37 @@ +import { updateAttachment } from '@documenso/lib/server-only/envelope-attachment/update-attachment'; + +import { authenticatedProcedure } from '../../trpc'; +import { + ZUpdateAttachmentRequestSchema, + ZUpdateAttachmentResponseSchema, +} from './update-attachment.types'; + +export const updateAttachmentRoute = authenticatedProcedure + .meta({ + openapi: { + method: 'POST', + path: '/envelope/attachment/update', + summary: 'Update attachment', + description: 'Update an existing attachment', + tags: ['Envelope'], + }, + }) + .input(ZUpdateAttachmentRequestSchema) + .output(ZUpdateAttachmentResponseSchema) + .mutation(async ({ input, ctx }) => { + const { teamId } = ctx; + const userId = ctx.user.id; + + const { id, data } = input; + + ctx.logger.info({ + input: { id }, + }); + + await updateAttachment({ + id, + userId, + teamId, + data, + }); + }); diff --git a/packages/trpc/server/envelope-router/attachment/update-attachment.types.ts b/packages/trpc/server/envelope-router/attachment/update-attachment.types.ts new file mode 100644 index 000000000..eaf12b559 --- /dev/null +++ b/packages/trpc/server/envelope-router/attachment/update-attachment.types.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +export const ZUpdateAttachmentRequestSchema = z.object({ + id: z.string(), + data: z.object({ + label: z.string().min(1, 'Label is required'), + data: z.string().url('Must be a valid URL'), + }), +}); + +export const ZUpdateAttachmentResponseSchema = z.void(); + +export type TUpdateAttachmentRequest = z.infer; +export type TUpdateAttachmentResponse = z.infer; diff --git a/packages/trpc/server/envelope-router/create-envelope.ts b/packages/trpc/server/envelope-router/create-envelope.ts index ce3bb0a47..517fd699d 100644 --- a/packages/trpc/server/envelope-router/create-envelope.ts +++ b/packages/trpc/server/envelope-router/create-envelope.ts @@ -9,7 +9,7 @@ import { } from './create-envelope.types'; export const createEnvelopeRoute = authenticatedProcedure - .input(ZCreateEnvelopeRequestSchema) // Note: Before releasing this to public, update the response schema to be correct. + .input(ZCreateEnvelopeRequestSchema) .output(ZCreateEnvelopeResponseSchema) .mutation(async ({ input, ctx }) => { const { user, teamId } = ctx; @@ -24,6 +24,7 @@ export const createEnvelopeRoute = authenticatedProcedure folderId, items, meta, + attachments, } = input; ctx.logger.info({ @@ -57,6 +58,7 @@ export const createEnvelopeRoute = authenticatedProcedure folderId, envelopeItems: items, }, + attachments, meta, normalizePdf: true, requestMetadata: ctx.metadata, diff --git a/packages/trpc/server/envelope-router/create-envelope.types.ts b/packages/trpc/server/envelope-router/create-envelope.types.ts index 5f1e04cbf..ea0fee260 100644 --- a/packages/trpc/server/envelope-router/create-envelope.types.ts +++ b/packages/trpc/server/envelope-router/create-envelope.types.ts @@ -7,6 +7,7 @@ import { } from '@documenso/lib/types/document-auth'; import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values'; import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta'; +import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment'; import { ZFieldHeightSchema, ZFieldPageNumberSchema, @@ -76,6 +77,15 @@ export const ZCreateEnvelopeRequestSchema = z.object({ ) .optional(), meta: ZDocumentMetaCreateSchema.optional(), + attachments: z + .array( + z.object({ + label: z.string().min(1, 'Label is required'), + data: z.string().url('Must be a valid URL'), + type: ZEnvelopeAttachmentTypeSchema.optional().default('link'), + }), + ) + .optional(), }); export const ZCreateEnvelopeResponseSchema = z.object({ diff --git a/packages/trpc/server/envelope-router/router.ts b/packages/trpc/server/envelope-router/router.ts index 0f57741f0..7f4f4c785 100644 --- a/packages/trpc/server/envelope-router/router.ts +++ b/packages/trpc/server/envelope-router/router.ts @@ -1,4 +1,8 @@ import { router } from '../trpc'; +import { createAttachmentRoute } from './attachment/create-attachment'; +import { deleteAttachmentRoute } from './attachment/delete-attachment'; +import { findAttachmentsRoute } from './attachment/find-attachments'; +import { updateAttachmentRoute } from './attachment/update-attachment'; import { createEnvelopeRoute } from './create-envelope'; import { createEnvelopeItemsRoute } from './create-envelope-items'; import { deleteEnvelopeRoute } from './delete-envelope'; @@ -35,4 +39,10 @@ export const envelopeRouter = router({ set: setEnvelopeFieldsRoute, sign: signEnvelopeFieldRoute, }, + attachment: { + find: findAttachmentsRoute, + create: createAttachmentRoute, + update: updateAttachmentRoute, + delete: deleteAttachmentRoute, + }, }); diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts index 68bbd2127..3cefb136f 100644 --- a/packages/trpc/server/template-router/router.ts +++ b/packages/trpc/server/template-router/router.ts @@ -235,6 +235,7 @@ export const templateRouter = router({ publicDescription, type, meta, + attachments, } = input; const fileName = title.endsWith('.pdf') ? title : `${title}.pdf`; @@ -268,6 +269,7 @@ export const templateRouter = router({ publicDescription, }, meta, + attachments, requestMetadata: ctx.metadata, }); diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts index a53e8ba8c..0783ef232 100644 --- a/packages/trpc/server/template-router/schema.ts +++ b/packages/trpc/server/template-router/schema.ts @@ -19,6 +19,7 @@ import { ZDocumentMetaTypedSignatureEnabledSchema, ZDocumentMetaUploadSignatureEnabledSchema, } from '@documenso/lib/types/document-meta'; +import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment'; import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta'; import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params'; import { @@ -197,6 +198,15 @@ export const ZCreateTemplateV2RequestSchema = z.object({ publicDescription: ZTemplatePublicDescriptionSchema.optional(), type: z.nativeEnum(TemplateType).optional(), meta: ZTemplateMetaUpsertSchema.optional(), + attachments: z + .array( + z.object({ + label: z.string().min(1, 'Label is required'), + data: z.string().url('Must be a valid URL'), + type: ZEnvelopeAttachmentTypeSchema.optional().default('link'), + }), + ) + .optional(), }); /**