mirror of
https://github.com/documenso/documenso.git
synced 2026-07-14 23:07:13 +10:00
feat: add create document beta endpoint (#1584)
This commit is contained in:
@@ -8,7 +8,7 @@ jobs:
|
||||
e2e_tests:
|
||||
name: 'E2E Tests'
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: warp-ubuntu-2204-x64-16x
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
@@ -50,17 +49,13 @@ export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps)
|
||||
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
let document: DocumentWithData | null = null;
|
||||
|
||||
if (!recipient) {
|
||||
document = await trpcClient.document.getDocumentById.query({
|
||||
documentId: row.id,
|
||||
});
|
||||
} else {
|
||||
document = await trpcClient.document.getDocumentByToken.query({
|
||||
token: recipient.token,
|
||||
});
|
||||
}
|
||||
const document = !recipient
|
||||
? await trpcClient.document.getDocumentById.query({
|
||||
documentId: row.id,
|
||||
})
|
||||
: await trpcClient.document.getDocumentByToken.query({
|
||||
token: recipient.token,
|
||||
});
|
||||
|
||||
const documentData = document?.documentData;
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
|
||||
import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
|
||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||
import {
|
||||
@@ -81,17 +80,13 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
||||
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
let document: DocumentWithData | null = null;
|
||||
|
||||
if (!recipient) {
|
||||
document = await trpcClient.document.getDocumentById.query({
|
||||
documentId: row.id,
|
||||
});
|
||||
} else {
|
||||
document = await trpcClient.document.getDocumentByToken.query({
|
||||
token: recipient.token,
|
||||
});
|
||||
}
|
||||
const document = !recipient
|
||||
? await trpcClient.document.getDocumentById.query({
|
||||
documentId: row.id,
|
||||
})
|
||||
: await trpcClient.document.getDocumentByToken.query({
|
||||
token: recipient.token,
|
||||
});
|
||||
|
||||
const documentData = document?.documentData;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import type { FindTemplateRow } from '@documenso/lib/server-only/template/find-templates';
|
||||
import type {
|
||||
Team,
|
||||
TeamProfile,
|
||||
@@ -15,6 +14,7 @@ import type {
|
||||
} from '@documenso/prisma/client';
|
||||
import { TemplateType } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { FindTemplateRow } from '@documenso/trpc/server/template-router/schema';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Switch } from '@documenso/ui/primitives/switch';
|
||||
|
||||
+1
-1
@@ -7,11 +7,11 @@ import { useLingui } from '@lingui/react';
|
||||
import { EditIcon, FileIcon, LinkIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react';
|
||||
|
||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||
import type { FindTemplateRow } from '@documenso/lib/server-only/template/find-templates';
|
||||
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
|
||||
import type { TemplateDirectLink } from '@documenso/prisma/client';
|
||||
import { TemplateType } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { FindTemplateRow } from '@documenso/trpc/server/template-router/schema';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
SKIP_QUERY_BATCH_META,
|
||||
} from '@documenso/lib/constants/trpc';
|
||||
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
|
||||
import type { TTemplate } from '@documenso/lib/types/template';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
@@ -32,7 +32,7 @@ import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type EditTemplateFormProps = {
|
||||
className?: string;
|
||||
initialTemplate: TemplateWithDetails;
|
||||
initialTemplate: TTemplate;
|
||||
isEnterprise: boolean;
|
||||
templateRootPath: string;
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import { AlertTriangle, Globe2Icon, InfoIcon, Link2Icon, Loader, LockIcon } from
|
||||
|
||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import type { FindTemplateRow } from '@documenso/lib/server-only/template/find-templates';
|
||||
import type { FindTemplateRow } from '@documenso/trpc/server/template-router/schema';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
|
||||
@@ -7,9 +7,9 @@ import { useSession } from 'next-auth/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { TTemplate } from '@documenso/lib/types/template';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
|
||||
import {
|
||||
DocumentFlowFormContainerActions,
|
||||
DocumentFlowFormContainerContent,
|
||||
@@ -41,7 +41,7 @@ export type TConfigureDirectTemplateFormSchema = z.infer<typeof ZConfigureDirect
|
||||
export type ConfigureDirectTemplateFormProps = {
|
||||
flowStep: DocumentFlowStep;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
template: Omit<TemplateWithDetails, 'user'>;
|
||||
template: Omit<TTemplate, 'user'>;
|
||||
directTemplateRecipient: Recipient & { fields: Field[] };
|
||||
initialEmail?: string;
|
||||
onSubmit: (_data: TConfigureDirectTemplateFormSchema) => void;
|
||||
|
||||
@@ -8,9 +8,9 @@ import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import type { TTemplate } from '@documenso/lib/types/template';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
import { type Recipient } from '@documenso/prisma/client';
|
||||
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
|
||||
@@ -28,7 +28,7 @@ import type { DirectTemplateLocalField } from './sign-direct-template';
|
||||
import { SignDirectTemplateForm } from './sign-direct-template';
|
||||
|
||||
export type TemplatesDirectPageViewProps = {
|
||||
template: Omit<TemplateWithDetails, 'user'>;
|
||||
template: Omit<TTemplate, 'user'>;
|
||||
directTemplateToken: string;
|
||||
directTemplateRecipient: Recipient & { fields: Field[] };
|
||||
};
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import type { TTemplate } from '@documenso/lib/types/template';
|
||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import type { Field, Recipient, Signature } from '@documenso/prisma/client';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
import type { TemplateWithDetails } from '@documenso/prisma/types/template';
|
||||
import type {
|
||||
TRemovedSignedFieldWithTokenMutationSchema,
|
||||
TSignFieldWithTokenMutationSchema,
|
||||
@@ -55,7 +55,7 @@ export type SignDirectTemplateFormProps = {
|
||||
flowStep: DocumentFlowStep;
|
||||
directRecipient: Recipient;
|
||||
directRecipientFields: Field[];
|
||||
template: Omit<TemplateWithDetails, 'user'>;
|
||||
template: Omit<TTemplate, 'user'>;
|
||||
onSubmit: (_data: DirectTemplateLocalField[]) => Promise<void>;
|
||||
};
|
||||
|
||||
|
||||
@@ -151,6 +151,7 @@ test('[DOCUMENT_FLOW]: add settings', async ({ page }) => {
|
||||
await expect(page.getByTestId('documentActionSelectValue')).not.toBeVisible();
|
||||
|
||||
// Save the settings by going to the next step.
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
|
||||
@@ -41,7 +41,10 @@ export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
},
|
||||
},
|
||||
|
||||
// {
|
||||
|
||||
@@ -4,6 +4,19 @@ import { DEFAULT_DOCUMENT_TIME_ZONE } from './time-zones';
|
||||
|
||||
export const DEFAULT_DOCUMENT_DATE_FORMAT = 'yyyy-MM-dd hh:mm a';
|
||||
|
||||
export const VALID_DATE_FORMAT_VALUES = [
|
||||
DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
'yyyy-MM-dd',
|
||||
'dd/MM/yyyy hh:mm a',
|
||||
'MM/dd/yyyy hh:mm a',
|
||||
'yyyy-MM-dd HH:mm',
|
||||
'yy-MM-dd hh:mm a',
|
||||
'yyyy-MM-dd HH:mm:ss',
|
||||
'MMMM dd, yyyy hh:mm a',
|
||||
'EEEE, MMMM dd, yyyy hh:mm a',
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
||||
] as const;
|
||||
|
||||
export const DATE_FORMATS = [
|
||||
{
|
||||
key: 'yyyy-MM-dd_hh:mm_a',
|
||||
@@ -55,7 +68,11 @@ export const DATE_FORMATS = [
|
||||
label: 'ISO 8601',
|
||||
value: "yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
||||
},
|
||||
];
|
||||
] satisfies {
|
||||
key: string;
|
||||
label: string;
|
||||
value: (typeof VALID_DATE_FORMAT_VALUES)[number];
|
||||
}[];
|
||||
|
||||
export const convertToLocalSystemFormat = (
|
||||
customText: string,
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentVisibility, TemplateMeta } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import type { TCreateDocumentV2Request } from '@documenso/trpc/server/document-router/schema';
|
||||
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import type { TDocumentFormValues } from '../../types/document-form-values';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapDocumentToWebhookDocumentPayload,
|
||||
} from '../../types/webhook-payload';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { putPdfFile } from '../../universal/upload/put-file';
|
||||
import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentDataId: string;
|
||||
normalizePdf?: boolean;
|
||||
data: {
|
||||
title: string;
|
||||
externalId?: string;
|
||||
visibility?: DocumentVisibility;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes;
|
||||
globalActionAuth?: TDocumentActionAuthTypes;
|
||||
formValues?: TDocumentFormValues;
|
||||
recipients: TCreateDocumentV2Request['recipients'];
|
||||
};
|
||||
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const createDocumentV2 = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentDataId,
|
||||
normalizePdf,
|
||||
data,
|
||||
meta,
|
||||
requestMetadata,
|
||||
}: CreateDocumentOptions) => {
|
||||
const { title, formValues } = data;
|
||||
|
||||
const team = teamId
|
||||
? await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
teamGlobalSettings: true,
|
||||
members: {
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
if (teamId !== undefined && !team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (normalizePdf) {
|
||||
const documentData = await prisma.documentData.findFirst({
|
||||
where: {
|
||||
id: documentDataId,
|
||||
},
|
||||
});
|
||||
|
||||
if (documentData) {
|
||||
const buffer = await getFile(documentData);
|
||||
|
||||
const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer));
|
||||
|
||||
const newDocumentData = await putPdfFile({
|
||||
name: title.endsWith('.pdf') ? title : `${title}.pdf`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(normalizedPdf),
|
||||
});
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
documentDataId = newDocumentData.id;
|
||||
}
|
||||
}
|
||||
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
globalAccessAuth: data?.globalAccessAuth || null,
|
||||
globalActionAuth: data?.globalActionAuth || null,
|
||||
});
|
||||
|
||||
const recipientsHaveActionAuth = data.recipients?.some((recipient) => recipient.actionAuth);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (authOptions.globalActionAuth || recipientsHaveActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const visibility = determineDocumentVisibility(
|
||||
team?.teamGlobalSettings?.documentVisibility,
|
||||
team?.members[0].role ?? TeamMemberRole.MEMBER,
|
||||
);
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.create({
|
||||
data: {
|
||||
title,
|
||||
externalId: data.externalId,
|
||||
documentDataId,
|
||||
userId,
|
||||
teamId,
|
||||
authOptions,
|
||||
visibility,
|
||||
formValues,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
documentMeta: {
|
||||
create: {
|
||||
...meta,
|
||||
signingOrder: meta?.signingOrder || undefined,
|
||||
emailSettings: meta?.emailSettings || undefined,
|
||||
language: meta?.language || team?.teamGlobalSettings?.documentLanguage,
|
||||
typedSignatureEnabled:
|
||||
meta?.typedSignatureEnabled ?? team?.teamGlobalSettings?.typedSignatureEnabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
(data.recipients || []).map(async (recipient) => {
|
||||
const recipientAuthOptions = createRecipientAuthOptions({
|
||||
accessAuth: recipient.accessAuth || null,
|
||||
actionAuth: recipient.actionAuth || null,
|
||||
});
|
||||
|
||||
await tx.recipient.create({
|
||||
data: {
|
||||
documentId: document.id,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
authOptions: recipientAuthOptions,
|
||||
fields: {
|
||||
createMany: {
|
||||
data: (recipient.fields || []).map((field) => ({
|
||||
documentId: document.id,
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Todo: Is it necessary to create a full audit log with all fields and recipients audit logs?
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||
documentId: document.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
title,
|
||||
source: {
|
||||
type: DocumentSource.DOCUMENT,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const createdDocument = await tx.document.findFirst({
|
||||
where: {
|
||||
id: document.id,
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!createdDocument) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return createdDocument;
|
||||
});
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentSource, DocumentVisibility, WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
import { DocumentSource, WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||
import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '../../types/webhook-payload';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { putPdfFile } from '../../universal/upload/put-file';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CreateDocumentOptions = {
|
||||
@@ -88,25 +89,6 @@ export const createDocument = async ({
|
||||
userTeamRole = teamWithUserRole.members[0]?.role;
|
||||
}
|
||||
|
||||
const determineVisibility = (
|
||||
globalVisibility: DocumentVisibility | null | undefined,
|
||||
userRole: TeamMemberRole,
|
||||
): DocumentVisibility => {
|
||||
if (globalVisibility) {
|
||||
return globalVisibility;
|
||||
}
|
||||
|
||||
if (userRole === TeamMemberRole.ADMIN) {
|
||||
return DocumentVisibility.ADMIN;
|
||||
}
|
||||
|
||||
if (userRole === TeamMemberRole.MANAGER) {
|
||||
return DocumentVisibility.MANAGER_AND_ABOVE;
|
||||
}
|
||||
|
||||
return DocumentVisibility.EVERYONE;
|
||||
};
|
||||
|
||||
if (normalizePdf) {
|
||||
const documentData = await prisma.documentData.findFirst({
|
||||
where: {
|
||||
@@ -138,7 +120,7 @@ export const createDocument = async ({
|
||||
documentDataId,
|
||||
userId,
|
||||
teamId,
|
||||
visibility: determineVisibility(
|
||||
visibility: determineDocumentVisibility(
|
||||
team?.teamGlobalSettings?.documentVisibility,
|
||||
userTeamRole ?? TeamMemberRole.MEMBER,
|
||||
),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
@@ -12,16 +11,14 @@ export interface CreateDocumentFieldsOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
fields: {
|
||||
fields: (TFieldAndMeta & {
|
||||
recipientId: number;
|
||||
type: FieldType;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
fieldMeta?: TFieldMetaSchema;
|
||||
}[];
|
||||
})[];
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use server';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import {
|
||||
DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
} from '@documenso/lib/constants/direct-templates';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
@@ -20,18 +18,12 @@ export type CreateTemplateDirectLinkOptions = {
|
||||
directRecipientId?: number;
|
||||
};
|
||||
|
||||
export const ZCreateTemplateDirectLinkResponseSchema = TemplateDirectLinkSchema;
|
||||
|
||||
export type TCreateTemplateDirectLinkResponse = z.infer<
|
||||
typeof ZCreateTemplateDirectLinkResponseSchema
|
||||
>;
|
||||
|
||||
export const createTemplateDirectLink = async ({
|
||||
templateId,
|
||||
userId,
|
||||
teamId,
|
||||
directRecipientId,
|
||||
}: CreateTemplateDirectLinkOptions): Promise<TCreateTemplateDirectLinkResponse> => {
|
||||
}: CreateTemplateDirectLinkOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { omit } from 'remeda';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
import { TemplateSchema } from '@documenso/prisma/generated/zod';
|
||||
import type { TDuplicateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema';
|
||||
|
||||
export type DuplicateTemplateOptions = TDuplicateTemplateMutationSchema & {
|
||||
@@ -12,15 +10,11 @@ export type DuplicateTemplateOptions = TDuplicateTemplateMutationSchema & {
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const ZDuplicateTemplateResponseSchema = TemplateSchema;
|
||||
|
||||
export type TDuplicateTemplateResponse = z.infer<typeof ZDuplicateTemplateResponseSchema>;
|
||||
|
||||
export const duplicateTemplate = async ({
|
||||
templateId,
|
||||
userId,
|
||||
teamId,
|
||||
}: DuplicateTemplateOptions): Promise<TDuplicateTemplateResponse> => {
|
||||
}: DuplicateTemplateOptions) => {
|
||||
const template = await prisma.template.findUnique({
|
||||
where: {
|
||||
id: templateId,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { match } from 'ts-pattern';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
@@ -8,18 +7,9 @@ import {
|
||||
TeamMemberRole,
|
||||
type Template,
|
||||
} from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentDataSchema,
|
||||
FieldSchema,
|
||||
RecipientSchema,
|
||||
TeamSchema,
|
||||
TemplateDirectLinkSchema,
|
||||
TemplateMetaSchema,
|
||||
TemplateSchema,
|
||||
} from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { type FindResultResponse, ZFindResultResponse } from '../../types/search-params';
|
||||
import { type FindResultResponse } from '../../types/search-params';
|
||||
|
||||
export type FindTemplatesOptions = {
|
||||
userId: number;
|
||||
@@ -29,36 +19,13 @@ export type FindTemplatesOptions = {
|
||||
perPage?: number;
|
||||
};
|
||||
|
||||
export const ZFindTemplatesResponseSchema = ZFindResultResponse.extend({
|
||||
data: TemplateSchema.extend({
|
||||
templateDocumentData: DocumentDataSchema,
|
||||
team: TeamSchema.pick({
|
||||
id: true,
|
||||
url: true,
|
||||
}).nullable(),
|
||||
fields: FieldSchema.array(),
|
||||
recipients: RecipientSchema.array(),
|
||||
templateMeta: TemplateMetaSchema.pick({
|
||||
signingOrder: true,
|
||||
distributionMethod: true,
|
||||
}).nullable(),
|
||||
directLink: TemplateDirectLinkSchema.pick({
|
||||
token: true,
|
||||
enabled: true,
|
||||
}).nullable(),
|
||||
}).array(), // Todo: openapi.
|
||||
});
|
||||
|
||||
export type TFindTemplatesResponse = z.infer<typeof ZFindTemplatesResponseSchema>;
|
||||
export type FindTemplateRow = TFindTemplatesResponse['data'][number];
|
||||
|
||||
export const findTemplates = async ({
|
||||
userId,
|
||||
teamId,
|
||||
type,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
}: FindTemplatesOptions): Promise<TFindTemplatesResponse> => {
|
||||
}: FindTemplatesOptions) => {
|
||||
const whereFilter: Prisma.TemplateWhereInput[] = [];
|
||||
|
||||
if (teamId === undefined) {
|
||||
@@ -112,7 +79,6 @@ export const findTemplates = async ({
|
||||
AND: whereFilter,
|
||||
},
|
||||
include: {
|
||||
templateDocumentData: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
DocumentDataSchema,
|
||||
FieldSchema,
|
||||
RecipientSchema,
|
||||
TemplateDirectLinkSchema,
|
||||
TemplateMetaSchema,
|
||||
TemplateSchema,
|
||||
UserSchema,
|
||||
} from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
@@ -19,26 +8,7 @@ export type GetTemplateByIdOptions = {
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const ZGetTemplateByIdResponseSchema = TemplateSchema.extend({
|
||||
directLink: TemplateDirectLinkSchema.nullable(),
|
||||
templateDocumentData: DocumentDataSchema,
|
||||
templateMeta: TemplateMetaSchema.nullable(),
|
||||
recipients: RecipientSchema.array(),
|
||||
fields: FieldSchema.array(),
|
||||
user: UserSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
}),
|
||||
});
|
||||
|
||||
export type TGetTemplateByIdResponse = z.infer<typeof ZGetTemplateByIdResponseSchema>;
|
||||
|
||||
export const getTemplateById = async ({
|
||||
id,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetTemplateByIdOptions): Promise<TGetTemplateByIdResponse> => {
|
||||
export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TemplateSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
@@ -11,15 +8,11 @@ export type MoveTemplateToTeamOptions = {
|
||||
userId: number;
|
||||
};
|
||||
|
||||
export const ZMoveTemplateToTeamResponseSchema = TemplateSchema;
|
||||
|
||||
export type TMoveTemplateToTeamResponse = z.infer<typeof ZMoveTemplateToTeamResponseSchema>;
|
||||
|
||||
export const moveTemplateToTeam = async ({
|
||||
templateId,
|
||||
teamId,
|
||||
userId,
|
||||
}: MoveTemplateToTeamOptions): Promise<TMoveTemplateToTeamResponse> => {
|
||||
}: MoveTemplateToTeamOptions) => {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const template = await tx.template.findFirst({
|
||||
where: {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
'use server';
|
||||
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
@@ -14,18 +11,12 @@ export type ToggleTemplateDirectLinkOptions = {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export const ZToggleTemplateDirectLinkResponseSchema = TemplateDirectLinkSchema;
|
||||
|
||||
export type TToggleTemplateDirectLinkResponse = z.infer<
|
||||
typeof ZToggleTemplateDirectLinkResponseSchema
|
||||
>;
|
||||
|
||||
export const toggleTemplateDirectLink = async ({
|
||||
templateId,
|
||||
userId,
|
||||
teamId,
|
||||
enabled,
|
||||
}: ToggleTemplateDirectLinkOptions): Promise<TToggleTemplateDirectLinkResponse> => {
|
||||
}: ToggleTemplateDirectLinkOptions) => {
|
||||
const template = await prisma.template.findFirst({
|
||||
where: {
|
||||
id: templateId,
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
'use server';
|
||||
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { DocumentVisibility, Template, TemplateMeta } from '@documenso/prisma/client';
|
||||
import { TemplateSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
@@ -28,17 +25,13 @@ export type UpdateTemplateOptions = {
|
||||
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
|
||||
};
|
||||
|
||||
export const ZUpdateTemplateResponseSchema = TemplateSchema;
|
||||
|
||||
export type TUpdateTemplateResponse = z.infer<typeof ZUpdateTemplateResponseSchema>;
|
||||
|
||||
export const updateTemplate = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
meta = {},
|
||||
data = {},
|
||||
}: UpdateTemplateOptions): Promise<TUpdateTemplateResponse> => {
|
||||
}: UpdateTemplateOptions) => {
|
||||
const template = await prisma.template.findFirstOrThrow({
|
||||
where: {
|
||||
id: templateId,
|
||||
|
||||
+428
-404
File diff suppressed because it is too large
Load Diff
+428
-403
File diff suppressed because it is too large
Load Diff
+428
-404
File diff suppressed because it is too large
Load Diff
+428
-404
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
export const ZBaseFieldMeta = z.object({
|
||||
label: z.string().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
@@ -114,3 +116,53 @@ export type TFieldMetaNotOptionalSchema = z.infer<typeof ZFieldMetaNotOptionalSc
|
||||
export const ZFieldMetaSchema = ZFieldMetaNotOptionalSchema.optional();
|
||||
|
||||
export type TFieldMetaSchema = z.infer<typeof ZFieldMetaSchema>;
|
||||
|
||||
export const ZFieldAndMetaSchema = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal(FieldType.SIGNATURE),
|
||||
// Do not use z.undefined(), or void since this will create an invalid schema for Speakeasy SDK generation.
|
||||
fieldMeta: z.literal(undefined),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.FREE_SIGNATURE),
|
||||
fieldMeta: z.literal(undefined), // Same as above.
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.INITIALS),
|
||||
fieldMeta: ZInitialsFieldMeta.optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.NAME),
|
||||
fieldMeta: ZNameFieldMeta.optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.EMAIL),
|
||||
fieldMeta: ZEmailFieldMeta.optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.DATE),
|
||||
fieldMeta: ZDateFieldMeta.optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.TEXT),
|
||||
fieldMeta: ZTextFieldMeta.optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.NUMBER),
|
||||
fieldMeta: ZNumberFieldMeta.optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.RADIO),
|
||||
fieldMeta: ZRadioFieldMeta.optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.CHECKBOX),
|
||||
fieldMeta: ZCheckboxFieldMeta.optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.DROPDOWN),
|
||||
fieldMeta: ZDropdownFieldMeta.optional(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type TFieldAndMeta = z.infer<typeof ZFieldAndMetaSchema>;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FieldSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
/**
|
||||
@@ -28,3 +30,22 @@ export const ZFieldSchema = FieldSchema.pick({
|
||||
inserted: true,
|
||||
fieldMeta: true,
|
||||
});
|
||||
|
||||
export const ZFieldPageNumberSchema = z
|
||||
.number()
|
||||
.min(1)
|
||||
.describe('The page number the field will be on.');
|
||||
|
||||
export const ZFieldPageXSchema = z
|
||||
.number()
|
||||
.min(0)
|
||||
.describe('The X coordinate of where the field will be placed.');
|
||||
|
||||
export const ZFieldPageYSchema = z
|
||||
.number()
|
||||
.min(0)
|
||||
.describe('The Y coordinate of where the field will be placed.');
|
||||
|
||||
export const ZFieldWidthSchema = z.number().min(1).describe('The width of the field.');
|
||||
|
||||
export const ZFieldHeightSchema = z.number().min(1).describe('The height of the field.');
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
import {
|
||||
DocumentDataSchema,
|
||||
TeamSchema,
|
||||
TemplateDirectLinkSchema,
|
||||
TemplateMetaSchema,
|
||||
TemplateSchema,
|
||||
UserSchema,
|
||||
} from '@documenso/prisma/generated/zod';
|
||||
|
||||
import { ZFieldSchema } from './field';
|
||||
import { ZRecipientLiteSchema } from './recipient';
|
||||
|
||||
/**
|
||||
* The full template response schema.
|
||||
*
|
||||
* Mainly used for returning a single template from the API.
|
||||
*/
|
||||
export const ZTemplateSchema = TemplateSchema.pick({
|
||||
type: true,
|
||||
visibility: true,
|
||||
id: true,
|
||||
externalId: true,
|
||||
title: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
authOptions: true,
|
||||
templateDocumentDataId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
publicTitle: true,
|
||||
publicDescription: true,
|
||||
}).extend({
|
||||
// Todo: Maybe we want to alter this a bit since this returns a lot of data.
|
||||
templateDocumentData: DocumentDataSchema.pick({
|
||||
type: true,
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
}),
|
||||
templateMeta: TemplateMetaSchema.pick({
|
||||
id: true,
|
||||
subject: true,
|
||||
message: true,
|
||||
timezone: true,
|
||||
dateFormat: true,
|
||||
signingOrder: true,
|
||||
typedSignatureEnabled: true,
|
||||
distributionMethod: true,
|
||||
templateId: true,
|
||||
redirectUrl: true,
|
||||
language: true,
|
||||
emailSettings: true,
|
||||
}).nullable(),
|
||||
directLink: TemplateDirectLinkSchema.nullable(),
|
||||
user: UserSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
}),
|
||||
recipients: ZRecipientLiteSchema.array(),
|
||||
fields: ZFieldSchema.array(),
|
||||
});
|
||||
|
||||
export type TTemplate = z.infer<typeof ZTemplateSchema>;
|
||||
|
||||
/**
|
||||
* A lite version of the template response schema without relations.
|
||||
*/
|
||||
export const ZTemplateLiteSchema = TemplateSchema.pick({
|
||||
type: true,
|
||||
visibility: true,
|
||||
id: true,
|
||||
externalId: true,
|
||||
title: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
authOptions: true,
|
||||
templateDocumentDataId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
publicTitle: true,
|
||||
publicDescription: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* A version of the template response schema when returning multiple template at once from a single API endpoint.
|
||||
*/
|
||||
export const ZTemplateManySchema = TemplateSchema.pick({
|
||||
type: true,
|
||||
visibility: true,
|
||||
id: true,
|
||||
externalId: true,
|
||||
title: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
authOptions: true,
|
||||
templateDocumentDataId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
publicTitle: true,
|
||||
publicDescription: true,
|
||||
}).extend({
|
||||
team: TeamSchema.pick({
|
||||
id: true,
|
||||
url: true,
|
||||
}).nullable(),
|
||||
fields: ZFieldSchema.array(),
|
||||
recipients: ZRecipientLiteSchema.array(),
|
||||
templateMeta: TemplateMetaSchema.pick({
|
||||
signingOrder: true,
|
||||
distributionMethod: true,
|
||||
}).nullable(),
|
||||
directLink: TemplateDirectLinkSchema.pick({
|
||||
token: true,
|
||||
enabled: true,
|
||||
}).nullable(),
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
export const determineDocumentVisibility = (
|
||||
globalVisibility: DocumentVisibility | null | undefined,
|
||||
userRole: TeamMemberRole,
|
||||
): DocumentVisibility => {
|
||||
if (globalVisibility) {
|
||||
return globalVisibility;
|
||||
}
|
||||
|
||||
if (userRole === TeamMemberRole.ADMIN) {
|
||||
return DocumentVisibility.ADMIN;
|
||||
}
|
||||
|
||||
if (userRole === TeamMemberRole.MANAGER) {
|
||||
return DocumentVisibility.MANAGER_AND_ABOVE;
|
||||
}
|
||||
|
||||
return DocumentVisibility.EVERYONE;
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { Document, DocumentData, DocumentMeta } from '@documenso/prisma/client';
|
||||
|
||||
export type DocumentWithData = Document & {
|
||||
documentData?: DocumentData | null;
|
||||
documentMeta?: DocumentMeta | null;
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { TGetTemplateByIdResponse } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import type { DocumentData, Template, TemplateMeta } from '@documenso/prisma/client';
|
||||
|
||||
export type TemplateWithData = Template & {
|
||||
templateDocumentData?: DocumentData | null;
|
||||
templateMeta?: TemplateMeta | null;
|
||||
};
|
||||
|
||||
export type TemplateWithDetails = TGetTemplateByIdResponse;
|
||||
@@ -7,8 +7,10 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
|
||||
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
|
||||
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
||||
import { createDocument } from '@documenso/lib/server-only/document/create-document';
|
||||
import { createDocumentV2 } from '@documenso/lib/server-only/document/create-document-v2';
|
||||
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
|
||||
import { duplicateDocument } from '@documenso/lib/server-only/document/duplicate-document-by-id';
|
||||
import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
|
||||
@@ -22,11 +24,14 @@ import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/
|
||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||
import { DocumentDataType, DocumentStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { authenticatedProcedure, procedure, router } from '../trpc';
|
||||
import {
|
||||
ZCreateDocumentRequestSchema,
|
||||
ZCreateDocumentV2RequestSchema,
|
||||
ZCreateDocumentV2ResponseSchema,
|
||||
ZDeleteDocumentMutationSchema,
|
||||
ZDistributeDocumentRequestSchema,
|
||||
ZDistributeDocumentResponseSchema,
|
||||
@@ -146,6 +151,79 @@ export const documentRouter = router({
|
||||
});
|
||||
}),
|
||||
|
||||
/**
|
||||
* Temporariy endpoint for V2 Beta until we allow passthrough documents on create.
|
||||
*
|
||||
* @public
|
||||
* @deprecated
|
||||
*/
|
||||
createDocumentTemporary: authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/document/create/beta',
|
||||
summary: 'Create document',
|
||||
description:
|
||||
'You will need to upload the PDF to the provided URL returned. Note: Once V2 API is released, this will be removed since we will allow direct uploads, instead of using an upload URL.',
|
||||
tags: ['Document'],
|
||||
},
|
||||
})
|
||||
.input(ZCreateDocumentV2RequestSchema)
|
||||
.output(ZCreateDocumentV2ResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx;
|
||||
|
||||
const {
|
||||
title,
|
||||
externalId,
|
||||
visibility,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
recipients,
|
||||
meta,
|
||||
} = input;
|
||||
|
||||
const { remaining } = await getServerLimits({ email: ctx.user.email, teamId });
|
||||
|
||||
if (remaining.documents <= 0) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'You have reached your document limit for this month. Please upgrade your plan.',
|
||||
});
|
||||
}
|
||||
|
||||
const fileName = title.endsWith('.pdf') ? title : `${title}.pdf`;
|
||||
|
||||
const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
|
||||
|
||||
const documentData = await createDocumentData({
|
||||
data: key,
|
||||
type: DocumentDataType.S3_PATH,
|
||||
});
|
||||
|
||||
const createdDocument = await createDocumentV2({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
documentDataId: documentData.id,
|
||||
normalizePdf: false, // Not normalizing because of presigned URL.
|
||||
data: {
|
||||
title,
|
||||
externalId,
|
||||
visibility,
|
||||
globalAccessAuth,
|
||||
globalActionAuth,
|
||||
recipients,
|
||||
},
|
||||
meta,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
document: createdDocument,
|
||||
uploadUrl: url,
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* Wait until RR7 so we can passthrough documents.
|
||||
*
|
||||
@@ -220,6 +298,7 @@ export const documentRouter = router({
|
||||
typedSignatureEnabled: meta.typedSignatureEnabled,
|
||||
redirectUrl: meta.redirectUrl,
|
||||
distributionMethod: meta.distributionMethod,
|
||||
signingOrder: meta.signingOrder,
|
||||
emailSettings: meta.emailSettings,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
@@ -240,8 +319,8 @@ export const documentRouter = router({
|
||||
deleteDocument: authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/document/{documentId}',
|
||||
method: 'POST',
|
||||
path: '/document/delete',
|
||||
summary: 'Delete document',
|
||||
tags: ['Document'],
|
||||
},
|
||||
@@ -320,6 +399,8 @@ export const documentRouter = router({
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* Todo: Remove and use `updateDocument` endpoint instead.
|
||||
*/
|
||||
setSigningOrderForDocument: authenticatedProcedure
|
||||
.input(ZSetSigningOrderForDocumentMutationSchema)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { VALID_DATE_FORMAT_VALUES } from '@documenso/lib/constants/date-formats';
|
||||
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
||||
import {
|
||||
ZDocumentLiteSchema,
|
||||
@@ -11,6 +12,15 @@ import {
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
|
||||
import {
|
||||
ZFieldHeightSchema,
|
||||
ZFieldPageNumberSchema,
|
||||
ZFieldPageXSchema,
|
||||
ZFieldPageYSchema,
|
||||
ZFieldWidthSchema,
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
import {
|
||||
@@ -22,14 +32,42 @@ import {
|
||||
FieldType,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
import { ZCreateRecipientSchema } from '../recipient-router/schema';
|
||||
|
||||
export const ZDocumentTitleSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(255)
|
||||
.describe('The title of the document.');
|
||||
|
||||
export const ZDocumentExternalIdSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.describe('The external ID of the document.');
|
||||
|
||||
export const ZDocumentVisibilitySchema = z
|
||||
.nativeEnum(DocumentVisibility)
|
||||
.describe('The visibility of the document.');
|
||||
|
||||
export const ZDocumentMetaTimezoneSchema = z
|
||||
.string()
|
||||
.describe('The timezone to use for date fields and signing the document.');
|
||||
.describe(
|
||||
'The timezone to use for date fields and signing the document. Example Etc/UTC, Australia/Melbourne',
|
||||
);
|
||||
// Cooked.
|
||||
// .refine((value) => TIME_ZONES.includes(value), {
|
||||
// message: 'Invalid timezone. Please provide a valid timezone',
|
||||
// });
|
||||
|
||||
export type TDocumentMetaTimezone = z.infer<typeof ZDocumentMetaTimezoneSchema>;
|
||||
|
||||
export const ZDocumentMetaDateFormatSchema = z
|
||||
.string()
|
||||
.enum(VALID_DATE_FORMAT_VALUES)
|
||||
.describe('The date format to use for date fields and signing the document.');
|
||||
|
||||
export type TDocumentMetaDateFormat = z.infer<typeof ZDocumentMetaDateFormatSchema>;
|
||||
|
||||
export const ZDocumentMetaRedirectUrlSchema = z
|
||||
.string()
|
||||
.describe('The URL to which the recipient should be redirected after signing the document.')
|
||||
@@ -55,7 +93,7 @@ export const ZDocumentMetaDistributionMethodSchema = z
|
||||
|
||||
export const ZDocumentMetaTypedSignatureEnabledSchema = z
|
||||
.boolean()
|
||||
.describe('Whether to allow typed signatures.');
|
||||
.describe('Whether to allow recipients to sign using a typed signature.');
|
||||
|
||||
export const ZFindDocumentsRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
templateId: z
|
||||
@@ -113,23 +151,79 @@ export const ZGetDocumentWithDetailsByIdRequestSchema = z.object({
|
||||
export const ZGetDocumentWithDetailsByIdResponseSchema = ZDocumentSchema;
|
||||
|
||||
export const ZCreateDocumentRequestSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
title: ZDocumentTitleSchema,
|
||||
documentDataId: z.string().min(1),
|
||||
timezone: z.string().optional(),
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZCreateDocumentV2RequestSchema = z.object({
|
||||
title: ZDocumentTitleSchema,
|
||||
externalId: ZDocumentExternalIdSchema.optional(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
||||
formValues: ZDocumentFormValuesSchema.optional(),
|
||||
recipients: z
|
||||
.array(
|
||||
ZCreateRecipientSchema.extend({
|
||||
fields: ZFieldAndMetaSchema.and(
|
||||
z.object({
|
||||
pageNumber: ZFieldPageNumberSchema,
|
||||
pageX: ZFieldPageXSchema,
|
||||
pageY: ZFieldPageYSchema,
|
||||
width: ZFieldWidthSchema,
|
||||
height: ZFieldHeightSchema,
|
||||
}),
|
||||
)
|
||||
.array()
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.refine(
|
||||
(recipients) => {
|
||||
const emails = recipients.map((recipient) => recipient.email);
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
{ message: 'Recipients must have unique emails' },
|
||||
)
|
||||
.optional(),
|
||||
meta: z
|
||||
.object({
|
||||
subject: ZDocumentMetaSubjectSchema.optional(),
|
||||
message: ZDocumentMetaMessageSchema.optional(),
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional(),
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
|
||||
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type TCreateDocumentV2Request = z.infer<typeof ZCreateDocumentV2RequestSchema>;
|
||||
|
||||
export const ZCreateDocumentV2ResponseSchema = z.object({
|
||||
document: ZDocumentSchema,
|
||||
uploadUrl: z
|
||||
.string()
|
||||
.describe(
|
||||
'The URL to upload the document PDF to. Use a PUT request with the file via form-data',
|
||||
),
|
||||
});
|
||||
|
||||
export const ZUpdateDocumentRequestSchema = z.object({
|
||||
documentId: z.number(),
|
||||
data: z
|
||||
.object({
|
||||
title: z.string().describe('The title of the document.').min(1).optional(),
|
||||
externalId: z.string().nullish().describe('The external ID of the document.'),
|
||||
visibility: z
|
||||
.nativeEnum(DocumentVisibility)
|
||||
.describe('The visibility of the document.')
|
||||
.optional(),
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
|
||||
title: ZDocumentTitleSchema.optional(),
|
||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
|
||||
})
|
||||
.optional(),
|
||||
meta: z
|
||||
@@ -139,6 +233,7 @@ export const ZUpdateDocumentRequestSchema = z.object({
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional(),
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
|
||||
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
|
||||
@@ -193,8 +193,8 @@ export const fieldRouter = router({
|
||||
deleteDocumentField: authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/document/field/{fieldId}',
|
||||
method: 'POST',
|
||||
path: '/document/field/delete',
|
||||
summary: 'Delete document field',
|
||||
tags: ['Document Fields'],
|
||||
},
|
||||
@@ -370,8 +370,8 @@ export const fieldRouter = router({
|
||||
deleteTemplateField: authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/template/field/{fieldId}',
|
||||
method: 'POST',
|
||||
path: '/template/field/delete',
|
||||
summary: 'Delete template field',
|
||||
tags: ['Template Fields'],
|
||||
},
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZRecipientActionAuthSchema } from '@documenso/lib/types/document-auth';
|
||||
import { ZFieldSchema } from '@documenso/lib/types/field';
|
||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import {
|
||||
ZFieldHeightSchema,
|
||||
ZFieldPageNumberSchema,
|
||||
ZFieldPageXSchema,
|
||||
ZFieldPageYSchema,
|
||||
ZFieldSchema,
|
||||
ZFieldWidthSchema,
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZFieldAndMetaSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
const ZCreateFieldSchema = z.object({
|
||||
recipientId: z.number().describe('The ID of the recipient to create the field for.'),
|
||||
type: ZFieldSchema.shape.type.describe('The type of the field to create.'),
|
||||
pageNumber: z.number().describe('The page number the field will be on.'),
|
||||
pageX: z.number().describe('The X coordinate of where the field will be placed.'),
|
||||
pageY: z.number().describe('The Y coordinate of where the field will be placed.'),
|
||||
width: z.number().describe('The width of the field.'),
|
||||
height: z.number().describe('The height of the field.'),
|
||||
fieldMeta: ZFieldMetaSchema.optional(),
|
||||
});
|
||||
const ZCreateFieldSchema = ZFieldAndMetaSchema.and(
|
||||
z.object({
|
||||
recipientId: z.number().describe('The ID of the recipient to create the field for.'),
|
||||
pageNumber: ZFieldPageNumberSchema,
|
||||
pageX: ZFieldPageXSchema,
|
||||
pageY: ZFieldPageYSchema,
|
||||
width: ZFieldWidthSchema,
|
||||
height: ZFieldHeightSchema,
|
||||
}),
|
||||
);
|
||||
|
||||
const ZUpdateFieldSchema = z.object({
|
||||
id: z.number().describe('The ID of the field to update.'),
|
||||
type: ZFieldSchema.shape.type.optional().describe('The type of the field to update.'),
|
||||
pageNumber: z.number().optional().describe('The page number the field will be on.'),
|
||||
pageX: z.number().optional().describe('The X coordinate of where the field will be placed.'),
|
||||
pageY: z.number().optional().describe('The Y coordinate of where the field will be placed.'),
|
||||
width: z.number().optional().describe('The width of the field.'),
|
||||
height: z.number().optional().describe('The height of the field.'),
|
||||
fieldMeta: ZFieldMetaSchema.optional(),
|
||||
});
|
||||
const ZUpdateFieldSchema = ZFieldAndMetaSchema.and(
|
||||
z.object({
|
||||
id: z.number().describe('The ID of the field to update.'),
|
||||
pageNumber: ZFieldPageNumberSchema.optional(),
|
||||
pageX: ZFieldPageXSchema.optional(),
|
||||
pageY: ZFieldPageYSchema.optional(),
|
||||
width: ZFieldWidthSchema.optional(),
|
||||
height: ZFieldHeightSchema.optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
export const ZCreateDocumentFieldRequestSchema = z.object({
|
||||
documentId: z.number(),
|
||||
|
||||
@@ -4,9 +4,27 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
|
||||
import { appRouter } from './router';
|
||||
|
||||
export const openApiDocument = generateOpenApiDocument(appRouter, {
|
||||
title: 'Documenso v2 beta API',
|
||||
description: 'Subject to breaking changes until v2 is fully released.',
|
||||
version: '0.0.0',
|
||||
baseUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta`,
|
||||
});
|
||||
export const openApiDocument = {
|
||||
...generateOpenApiDocument(appRouter, {
|
||||
title: 'Documenso v2 beta API',
|
||||
description: 'Subject to breaking changes until v2 is fully released.',
|
||||
version: '0.0.0',
|
||||
baseUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta`,
|
||||
securitySchemes: {
|
||||
apiKey: {
|
||||
type: 'apiKey',
|
||||
in: 'header',
|
||||
name: 'Authorization',
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* Dirty way to pass through the security field.
|
||||
*/
|
||||
security: [
|
||||
{
|
||||
apiKey: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -193,8 +193,8 @@ export const recipientRouter = router({
|
||||
deleteDocumentRecipient: authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/document/recipient/{recipientId}',
|
||||
method: 'POST',
|
||||
path: '/document/recipient/delete',
|
||||
summary: 'Delete document recipient',
|
||||
tags: ['Document Recipients'],
|
||||
},
|
||||
@@ -367,8 +367,8 @@ export const recipientRouter = router({
|
||||
deleteTemplateRecipient: authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/template/recipient/{recipientId}',
|
||||
method: 'POST',
|
||||
path: '/template/recipient/delete',
|
||||
summary: 'Delete template recipient',
|
||||
tags: ['Template Recipients'],
|
||||
},
|
||||
|
||||
@@ -14,7 +14,14 @@ export const ZGetRecipientRequestSchema = z.object({
|
||||
|
||||
export const ZGetRecipientResponseSchema = ZRecipientSchema;
|
||||
|
||||
const ZCreateRecipientSchema = z.object({
|
||||
/**
|
||||
* When changing this, ensure everything that uses this schema is updated correctly
|
||||
* since this will change the Openapi schema.
|
||||
*
|
||||
* Example `createDocument` uses this, so you will need to update that function to
|
||||
* pass along required details.
|
||||
*/
|
||||
export const ZCreateRecipientSchema = z.object({
|
||||
email: z.string().toLowerCase().email().min(1),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
@@ -42,11 +49,16 @@ export const ZCreateDocumentRecipientResponseSchema = ZRecipientLiteSchema;
|
||||
|
||||
export const ZCreateDocumentRecipientsRequestSchema = z.object({
|
||||
documentId: z.number(),
|
||||
recipients: z.array(ZCreateRecipientSchema).refine((recipients) => {
|
||||
const emails = recipients.map((recipient) => recipient.email.toLowerCase());
|
||||
recipients: z.array(ZCreateRecipientSchema).refine(
|
||||
(recipients) => {
|
||||
const emails = recipients.map((recipient) => recipient.email.toLowerCase());
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
}),
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
{
|
||||
message: 'Recipients must have unique emails',
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const ZCreateDocumentRecipientsResponseSchema = z.object({
|
||||
@@ -62,13 +74,18 @@ export const ZUpdateDocumentRecipientResponseSchema = ZRecipientSchema;
|
||||
|
||||
export const ZUpdateDocumentRecipientsRequestSchema = z.object({
|
||||
documentId: z.number(),
|
||||
recipients: z.array(ZUpdateRecipientSchema).refine((recipients) => {
|
||||
const emails = recipients
|
||||
.filter((recipient) => recipient.email !== undefined)
|
||||
.map((recipient) => recipient.email?.toLowerCase());
|
||||
recipients: z.array(ZUpdateRecipientSchema).refine(
|
||||
(recipients) => {
|
||||
const emails = recipients
|
||||
.filter((recipient) => recipient.email !== undefined)
|
||||
.map((recipient) => recipient.email?.toLowerCase());
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
}),
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
{
|
||||
message: 'Recipients must have unique emails',
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const ZUpdateDocumentRecipientsResponseSchema = z.object({
|
||||
@@ -100,7 +117,7 @@ export const ZSetDocumentRecipientsRequestSchema = z
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
// Dirty hack to handle errors when .root is populated for an array type
|
||||
{ message: 'Signers must have unique emails', path: ['signers__root'] },
|
||||
{ message: 'Recipients must have unique emails', path: ['recipients__root'] },
|
||||
);
|
||||
|
||||
export const ZSetDocumentRecipientsResponseSchema = z.object({
|
||||
@@ -116,11 +133,16 @@ export const ZCreateTemplateRecipientResponseSchema = ZRecipientLiteSchema;
|
||||
|
||||
export const ZCreateTemplateRecipientsRequestSchema = z.object({
|
||||
templateId: z.number(),
|
||||
recipients: z.array(ZCreateRecipientSchema).refine((recipients) => {
|
||||
const emails = recipients.map((recipient) => recipient.email);
|
||||
recipients: z.array(ZCreateRecipientSchema).refine(
|
||||
(recipients) => {
|
||||
const emails = recipients.map((recipient) => recipient.email);
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
}),
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
{
|
||||
message: 'Recipients must have unique emails',
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const ZCreateTemplateRecipientsResponseSchema = z.object({
|
||||
@@ -136,13 +158,18 @@ export const ZUpdateTemplateRecipientResponseSchema = ZRecipientSchema;
|
||||
|
||||
export const ZUpdateTemplateRecipientsRequestSchema = z.object({
|
||||
templateId: z.number(),
|
||||
recipients: z.array(ZUpdateRecipientSchema).refine((recipients) => {
|
||||
const emails = recipients
|
||||
.filter((recipient) => recipient.email !== undefined)
|
||||
.map((recipient) => recipient.email);
|
||||
recipients: z.array(ZUpdateRecipientSchema).refine(
|
||||
(recipients) => {
|
||||
const emails = recipients
|
||||
.filter((recipient) => recipient.email !== undefined)
|
||||
.map((recipient) => recipient.email);
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
}),
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
{
|
||||
message: 'Recipients must have unique emails',
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const ZUpdateTemplateRecipientsResponseSchema = z.object({
|
||||
|
||||
@@ -13,36 +13,15 @@ import {
|
||||
ZCreateTemplateResponseSchema,
|
||||
createTemplate,
|
||||
} from '@documenso/lib/server-only/template/create-template';
|
||||
import {
|
||||
ZCreateTemplateDirectLinkResponseSchema,
|
||||
createTemplateDirectLink,
|
||||
} from '@documenso/lib/server-only/template/create-template-direct-link';
|
||||
import { createTemplateDirectLink } from '@documenso/lib/server-only/template/create-template-direct-link';
|
||||
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
|
||||
import { deleteTemplateDirectLink } from '@documenso/lib/server-only/template/delete-template-direct-link';
|
||||
import {
|
||||
ZDuplicateTemplateResponseSchema,
|
||||
duplicateTemplate,
|
||||
} from '@documenso/lib/server-only/template/duplicate-template';
|
||||
import {
|
||||
ZFindTemplatesResponseSchema,
|
||||
findTemplates,
|
||||
} from '@documenso/lib/server-only/template/find-templates';
|
||||
import {
|
||||
ZGetTemplateByIdResponseSchema,
|
||||
getTemplateById,
|
||||
} from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import {
|
||||
ZMoveTemplateToTeamResponseSchema,
|
||||
moveTemplateToTeam,
|
||||
} from '@documenso/lib/server-only/template/move-template-to-team';
|
||||
import {
|
||||
ZToggleTemplateDirectLinkResponseSchema,
|
||||
toggleTemplateDirectLink,
|
||||
} from '@documenso/lib/server-only/template/toggle-template-direct-link';
|
||||
import {
|
||||
ZUpdateTemplateResponseSchema,
|
||||
updateTemplate,
|
||||
} from '@documenso/lib/server-only/template/update-template';
|
||||
import { duplicateTemplate } from '@documenso/lib/server-only/template/duplicate-template';
|
||||
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import { moveTemplateToTeam } from '@documenso/lib/server-only/template/move-template-to-team';
|
||||
import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link';
|
||||
import { updateTemplate } from '@documenso/lib/server-only/template/update-template';
|
||||
import type { Document } from '@documenso/prisma/client';
|
||||
|
||||
import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc';
|
||||
@@ -50,16 +29,23 @@ import {
|
||||
ZCreateDocumentFromDirectTemplateRequestSchema,
|
||||
ZCreateDocumentFromTemplateRequestSchema,
|
||||
ZCreateDocumentFromTemplateResponseSchema,
|
||||
ZCreateTemplateDirectLinkMutationSchema,
|
||||
ZCreateTemplateDirectLinkRequestSchema,
|
||||
ZCreateTemplateDirectLinkResponseSchema,
|
||||
ZCreateTemplateMutationSchema,
|
||||
ZDeleteTemplateDirectLinkMutationSchema,
|
||||
ZDeleteTemplateDirectLinkRequestSchema,
|
||||
ZDeleteTemplateMutationSchema,
|
||||
ZDuplicateTemplateMutationSchema,
|
||||
ZDuplicateTemplateResponseSchema,
|
||||
ZFindTemplatesRequestSchema,
|
||||
ZFindTemplatesResponseSchema,
|
||||
ZGetTemplateByIdRequestSchema,
|
||||
ZMoveTemplatesToTeamRequestSchema,
|
||||
ZToggleTemplateDirectLinkMutationSchema,
|
||||
ZGetTemplateByIdResponseSchema,
|
||||
ZMoveTemplateToTeamRequestSchema,
|
||||
ZMoveTemplateToTeamResponseSchema,
|
||||
ZToggleTemplateDirectLinkRequestSchema,
|
||||
ZToggleTemplateDirectLinkResponseSchema,
|
||||
ZUpdateTemplateRequestSchema,
|
||||
ZUpdateTemplateResponseSchema,
|
||||
} from './schema';
|
||||
|
||||
export const templateRouter = router({
|
||||
@@ -202,8 +188,8 @@ export const templateRouter = router({
|
||||
deleteTemplate: authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/template/{templateId}',
|
||||
method: 'POST',
|
||||
path: '/template/delete',
|
||||
summary: 'Delete template',
|
||||
tags: ['Template'],
|
||||
},
|
||||
@@ -331,7 +317,7 @@ export const templateRouter = router({
|
||||
tags: ['Template'],
|
||||
},
|
||||
})
|
||||
.input(ZCreateTemplateDirectLinkMutationSchema)
|
||||
.input(ZCreateTemplateDirectLinkRequestSchema)
|
||||
.output(ZCreateTemplateDirectLinkResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx;
|
||||
@@ -358,14 +344,14 @@ export const templateRouter = router({
|
||||
deleteTemplateDirectLink: authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/template/direct/{templateId}',
|
||||
method: 'POST',
|
||||
path: '/template/direct/delete',
|
||||
summary: 'Delete direct link',
|
||||
description: 'Delete a direct link for a template',
|
||||
tags: ['Template'],
|
||||
},
|
||||
})
|
||||
.input(ZDeleteTemplateDirectLinkMutationSchema)
|
||||
.input(ZDeleteTemplateDirectLinkRequestSchema)
|
||||
.output(z.void())
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx;
|
||||
@@ -389,7 +375,7 @@ export const templateRouter = router({
|
||||
tags: ['Template'],
|
||||
},
|
||||
})
|
||||
.input(ZToggleTemplateDirectLinkMutationSchema)
|
||||
.input(ZToggleTemplateDirectLinkRequestSchema)
|
||||
.output(ZToggleTemplateDirectLinkResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx;
|
||||
@@ -413,7 +399,7 @@ export const templateRouter = router({
|
||||
tags: ['Template'],
|
||||
},
|
||||
})
|
||||
.input(ZMoveTemplatesToTeamRequestSchema)
|
||||
.input(ZMoveTemplateToTeamRequestSchema)
|
||||
.output(ZMoveTemplateToTeamResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { templateId, teamId } = input;
|
||||
|
||||
@@ -6,8 +6,14 @@ import {
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import {
|
||||
ZTemplateLiteSchema,
|
||||
ZTemplateManySchema,
|
||||
ZTemplateSchema,
|
||||
} from '@documenso/lib/types/template';
|
||||
import { DocumentSigningOrder, DocumentVisibility, TemplateType } from '@documenso/prisma/client';
|
||||
import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod';
|
||||
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
@@ -69,7 +75,9 @@ export const ZDuplicateTemplateMutationSchema = z.object({
|
||||
templateId: z.number(),
|
||||
});
|
||||
|
||||
export const ZCreateTemplateDirectLinkMutationSchema = z.object({
|
||||
export const ZDuplicateTemplateResponseSchema = ZTemplateLiteSchema;
|
||||
|
||||
export const ZCreateTemplateDirectLinkRequestSchema = z.object({
|
||||
templateId: z.number(),
|
||||
directRecipientId: z
|
||||
.number()
|
||||
@@ -79,15 +87,28 @@ export const ZCreateTemplateDirectLinkMutationSchema = z.object({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const ZDeleteTemplateDirectLinkMutationSchema = z.object({
|
||||
const GenericDirectLinkResponseSchema = TemplateDirectLinkSchema.pick({
|
||||
id: true,
|
||||
templateId: true,
|
||||
token: true,
|
||||
createdAt: true,
|
||||
enabled: true,
|
||||
directTemplateRecipientId: true,
|
||||
});
|
||||
|
||||
export const ZCreateTemplateDirectLinkResponseSchema = GenericDirectLinkResponseSchema;
|
||||
|
||||
export const ZDeleteTemplateDirectLinkRequestSchema = z.object({
|
||||
templateId: z.number(),
|
||||
});
|
||||
|
||||
export const ZToggleTemplateDirectLinkMutationSchema = z.object({
|
||||
export const ZToggleTemplateDirectLinkRequestSchema = z.object({
|
||||
templateId: z.number(),
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
|
||||
export const ZToggleTemplateDirectLinkResponseSchema = GenericDirectLinkResponseSchema;
|
||||
|
||||
export const ZDeleteTemplateMutationSchema = z.object({
|
||||
templateId: z.number(),
|
||||
});
|
||||
@@ -141,19 +162,32 @@ export const ZUpdateTemplateRequestSchema = z.object({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const ZUpdateTemplateResponseSchema = ZTemplateLiteSchema;
|
||||
|
||||
export const ZFindTemplatesRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
type: z.nativeEnum(TemplateType).describe('Filter templates by type.').optional(),
|
||||
});
|
||||
|
||||
export const ZFindTemplatesResponseSchema = ZFindResultResponse.extend({
|
||||
data: ZTemplateManySchema.array(),
|
||||
});
|
||||
|
||||
export type TFindTemplatesResponse = z.infer<typeof ZFindTemplatesResponseSchema>;
|
||||
export type FindTemplateRow = TFindTemplatesResponse['data'][number];
|
||||
|
||||
export const ZGetTemplateByIdRequestSchema = z.object({
|
||||
templateId: z.number(),
|
||||
});
|
||||
|
||||
export const ZMoveTemplatesToTeamRequestSchema = z.object({
|
||||
export const ZGetTemplateByIdResponseSchema = ZTemplateSchema;
|
||||
|
||||
export const ZMoveTemplateToTeamRequestSchema = z.object({
|
||||
templateId: z.number().describe('The ID of the template to move to.'),
|
||||
teamId: z.number().describe('The ID of the team to move the template to.'),
|
||||
});
|
||||
|
||||
export const ZMoveTemplateToTeamResponseSchema = ZTemplateLiteSchema;
|
||||
|
||||
export type TCreateTemplateMutationSchema = z.infer<typeof ZCreateTemplateMutationSchema>;
|
||||
export type TDuplicateTemplateMutationSchema = z.infer<typeof ZDuplicateTemplateMutationSchema>;
|
||||
export type TDeleteTemplateMutationSchema = z.infer<typeof ZDeleteTemplateMutationSchema>;
|
||||
|
||||
@@ -11,10 +11,10 @@ import { match } from 'ts-pattern';
|
||||
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import type { TDocument } from '@documenso/lib/types/document';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, type Field, type Recipient, SendStatus } from '@documenso/prisma/client';
|
||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||
import {
|
||||
DocumentGlobalAuthAccessSelect,
|
||||
DocumentGlobalAuthAccessTooltip,
|
||||
@@ -65,7 +65,7 @@ export type AddSettingsFormProps = {
|
||||
fields: Field[];
|
||||
isDocumentEnterprise: boolean;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
document: DocumentWithData;
|
||||
document: TDocument;
|
||||
currentTeamMemberRole?: TeamMemberRole;
|
||||
onSubmit: (_data: TAddSettingsFormSchema) => void;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} from '@documenso/trpc/server/document-router/schema';
|
||||
|
||||
export const ZMapNegativeOneToUndefinedSchema = z
|
||||
.string()
|
||||
@@ -32,8 +36,8 @@ export const ZAddSettingsFormSchema = z.object({
|
||||
ZDocumentActionAuthTypesSchema.optional(),
|
||||
),
|
||||
meta: z.object({
|
||||
timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||
dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||
redirectUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import type { TDocument } from '@documenso/lib/types/document';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
DocumentStatus,
|
||||
RecipientRole,
|
||||
} from '@documenso/prisma/client';
|
||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||
|
||||
@@ -43,7 +43,7 @@ export type AddSubjectFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
document: DocumentWithData;
|
||||
document: TDocument;
|
||||
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
};
|
||||
|
||||
@@ -14,10 +14,11 @@ import { DOCUMENT_DISTRIBUTION_METHODS } from '@documenso/lib/constants/document
|
||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import type { TTemplate } from '@documenso/lib/types/template';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { DocumentDistributionMethod, type Field, type Recipient } from '@documenso/prisma/client';
|
||||
import type { TemplateWithData } from '@documenso/prisma/types/template';
|
||||
import type { TDocumentMetaDateFormat } from '@documenso/trpc/server/document-router/schema';
|
||||
import {
|
||||
DocumentGlobalAuthAccessSelect,
|
||||
DocumentGlobalAuthAccessTooltip,
|
||||
@@ -71,7 +72,7 @@ export type AddTemplateSettingsFormProps = {
|
||||
fields: Field[];
|
||||
isEnterprise: boolean;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
template: TemplateWithData;
|
||||
template: TTemplate;
|
||||
currentTeamMemberRole?: TeamMemberRole;
|
||||
onSubmit: (_data: TAddTemplateSettingsFormSchema) => void;
|
||||
};
|
||||
@@ -104,7 +105,9 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
subject: template.templateMeta?.subject ?? '',
|
||||
message: template.templateMeta?.message ?? '',
|
||||
timezone: template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
|
||||
dateFormat: template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
dateFormat: (template.templateMeta?.dateFormat ??
|
||||
DEFAULT_DOCUMENT_DATE_FORMAT) as TDocumentMetaDateFormat,
|
||||
distributionMethod:
|
||||
template.templateMeta?.distributionMethod || DocumentDistributionMethod.EMAIL,
|
||||
redirectUrl: template.templateMeta?.redirectUrl ?? '',
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
|
||||
import { DocumentVisibility } from '@documenso/prisma/client';
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} from '@documenso/trpc/server/document-router/schema';
|
||||
|
||||
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
|
||||
import { DocumentDistributionMethod } from '.prisma/client';
|
||||
@@ -27,8 +31,8 @@ export const ZAddTemplateSettingsFormSchema = z.object({
|
||||
meta: z.object({
|
||||
subject: z.string(),
|
||||
message: z.string(),
|
||||
timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||
dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||
timezone: ZDocumentMetaTimezoneSchema.default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||
distributionMethod: z
|
||||
.nativeEnum(DocumentDistributionMethod)
|
||||
.optional()
|
||||
|
||||
Reference in New Issue
Block a user