diff --git a/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx index f728174a6..315c2022b 100644 --- a/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx @@ -97,6 +97,7 @@ export const DataTableActionDropdown = ({ diff --git a/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx index b31ad2048..9ded79fa5 100644 --- a/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx @@ -14,11 +14,17 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; type DeleteTemplateDialogProps = { id: number; + teamId?: number; open: boolean; onOpenChange: (_open: boolean) => void; }; -export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateDialogProps) => { +export const DeleteTemplateDialog = ({ + id, + teamId, + open, + onOpenChange, +}: DeleteTemplateDialogProps) => { const router = useRouter(); const { toast } = useToast(); @@ -67,7 +73,12 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD Cancel - diff --git a/packages/api/v1/contract.ts b/packages/api/v1/contract.ts index 577143ead..c6c6b47f5 100644 --- a/packages/api/v1/contract.ts +++ b/packages/api/v1/contract.ts @@ -15,10 +15,15 @@ import { ZGenerateDocumentFromTemplateMutationResponseSchema, ZGenerateDocumentFromTemplateMutationSchema, ZGetDocumentsQuerySchema, + ZGetTemplatesQuerySchema, + ZNoBodyMutationSchema, ZSendDocumentForSigningMutationSchema, + ZSuccessfulDeleteTemplateResponseSchema, ZSuccessfulDocumentResponseSchema, ZSuccessfulFieldResponseSchema, ZSuccessfulGetDocumentResponseSchema, + ZSuccessfulGetTemplateResponseSchema, + ZSuccessfulGetTemplatesResponseSchema, ZSuccessfulRecipientResponseSchema, ZSuccessfulResponseSchema, ZSuccessfulSigningResponseSchema, @@ -77,6 +82,41 @@ export const ApiContractV1 = c.router( summary: 'Upload a new document and get a presigned URL', }, + deleteTemplate: { + method: 'DELETE', + path: '/api/v1/templates/:id', + body: ZNoBodyMutationSchema, + responses: { + 200: ZSuccessfulDeleteTemplateResponseSchema, + 401: ZUnsuccessfulResponseSchema, + 404: ZUnsuccessfulResponseSchema, + }, + summary: 'Delete a template', + }, + + getTemplate: { + method: 'GET', + path: '/api/v1/templates/:id', + responses: { + 200: ZSuccessfulGetTemplateResponseSchema, + 401: ZUnsuccessfulResponseSchema, + 404: ZUnsuccessfulResponseSchema, + }, + summary: 'Get a single template', + }, + + getTemplates: { + method: 'GET', + path: '/api/v1/templates', + query: ZGetTemplatesQuerySchema, + responses: { + 200: ZSuccessfulGetTemplatesResponseSchema, + 401: ZUnsuccessfulResponseSchema, + 404: ZUnsuccessfulResponseSchema, + }, + summary: 'Get all templates', + }, + createDocumentFromTemplate: { method: 'POST', path: '/api/v1/templates/:templateId/create-document', diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts index 18221edb6..1379b447c 100644 --- a/packages/api/v1/implementation.ts +++ b/packages/api/v1/implementation.ts @@ -24,6 +24,9 @@ import { updateRecipient } from '@documenso/lib/server-only/recipient/update-rec import type { CreateDocumentFromTemplateResponse } from '@documenso/lib/server-only/template/create-document-from-template'; import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/template/create-document-from-template-legacy'; +import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template'; +import { findTemplates } from '@documenso/lib/server-only/template/find-templates'; +import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { getFile } from '@documenso/lib/universal/upload/get-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; @@ -277,6 +280,73 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { } }), + deleteTemplate: authenticatedMiddleware(async (args, user, team) => { + const { id: templateId } = args.params; + + try { + const deletedTemplate = await deleteTemplate({ + id: Number(templateId), + userId: user.id, + teamId: team?.id, + }); + + return { + status: 200, + body: deletedTemplate, + }; + } catch (err) { + return { + status: 404, + body: { + message: 'Template not found', + }, + }; + } + }), + + getTemplate: authenticatedMiddleware(async (args, user, team) => { + const { id: templateId } = args.params; + + try { + const template = await getTemplateById({ + id: Number(templateId), + userId: user.id, + teamId: team?.id, + }); + + return { + status: 200, + body: template, + }; + } catch (err) { + return AppError.toRestAPIError(err); + } + }), + + getTemplates: authenticatedMiddleware(async (args, user, team) => { + const page = Number(args.query.page) || 1; + const perPage = Number(args.query.perPage) || 10; + + try { + const { templates, totalPages } = await findTemplates({ + page, + perPage, + userId: user.id, + teamId: team?.id, + }); + + return { + status: 200, + body: { + templates, + totalPages, + }, + }; + } catch (err) { + return AppError.toRestAPIError(err); + } + }), + createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => { const { body, params } = args; diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts index 7f82c611e..5ecab08c6 100644 --- a/packages/api/v1/schema.ts +++ b/packages/api/v1/schema.ts @@ -2,13 +2,17 @@ import { z } from 'zod'; import { ZUrlSchema } from '@documenso/lib/schemas/common'; import { + DocumentDataType, FieldType, ReadStatus, RecipientRole, SendStatus, SigningStatus, + TemplateType, } from '@documenso/prisma/client'; +export const ZNoBodyMutationSchema = null; + /** * Documents */ @@ -315,3 +319,106 @@ export const ZUnsuccessfulResponseSchema = z.object({ }); export type TUnsuccessfulResponseSchema = z.infer; + +export const ZTemplateMetaSchema = z.object({ + id: z.string(), + subject: z.string().nullish(), + message: z.string().nullish(), + timezone: z.string().nullish(), + dateFormat: z.string().nullish(), + templateId: z.number(), + redirectUrl: z.string().nullish(), +}); + +export const ZTemplateSchema = z.object({ + id: z.number(), + type: z.nativeEnum(TemplateType), + title: z.string(), + userId: z.number(), + teamId: z.number().nullish(), + templateDocumentDataId: z.string(), + createdAt: z.date(), + updatedAt: z.date(), +}); + +export const ZRecipientSchema = z.object({ + id: z.number(), + documentId: z.number().nullish(), + templateId: z.number().nullish(), + email: z.string().email().min(1), + name: z.string(), + token: z.string(), + documentDeletedAt: z.date().nullish(), + expired: z.date().nullish(), + signedAt: z.date().nullish(), + authOptions: z.unknown(), + role: z.nativeEnum(RecipientRole), + readStatus: z.nativeEnum(ReadStatus), + signingStatus: z.nativeEnum(SigningStatus), + sendStatus: z.nativeEnum(SendStatus), +}); + +export const ZFieldSchema = z.object({ + id: z.number(), + secondaryId: z.string(), + documentId: z.number().nullish(), + templateId: z.number().nullish(), + recipientId: z.number(), + type: z.nativeEnum(FieldType), + page: z.number(), + positionX: z.unknown(), + positionY: z.unknown(), + width: z.unknown(), + height: z.unknown(), + customText: z.string(), + inserted: z.boolean(), +}); + +export const ZTemplateWithDataSchema = ZTemplateSchema.extend({ + templateMeta: ZTemplateMetaSchema.nullish(), + directLink: z + .object({ + token: z.string(), + enabled: z.boolean(), + }) + .nullable(), + templateDocumentData: z.object({ + id: z.string(), + type: z.nativeEnum(DocumentDataType), + data: z.string(), + }), + Field: ZFieldSchema.pick({ + id: true, + recipientId: true, + type: true, + page: true, + positionX: true, + positionY: true, + width: true, + height: true, + }).array(), + Recipient: ZRecipientSchema.pick({ + id: true, + email: true, + name: true, + authOptions: true, + role: true, + }).array(), +}); + +export const ZSuccessfulGetTemplateResponseSchema = ZTemplateWithDataSchema; + +export const ZSuccessfulDeleteTemplateResponseSchema = ZTemplateSchema; + +export const ZSuccessfulGetTemplatesResponseSchema = z.object({ + templates: ZTemplateWithDataSchema.omit({ + templateDocumentData: true, + templateMeta: true, + }).array(), + totalPages: z.number(), +}); + +export const ZGetTemplatesQuerySchema = z.object({ + page: z.coerce.number().min(1).optional().default(1), + perPage: z.coerce.number().min(1).optional().default(1), +}); diff --git a/packages/lib/server-only/template/delete-template.ts b/packages/lib/server-only/template/delete-template.ts index c24cc1333..0962b6b9a 100644 --- a/packages/lib/server-only/template/delete-template.ts +++ b/packages/lib/server-only/template/delete-template.ts @@ -5,26 +5,33 @@ import { prisma } from '@documenso/prisma'; export type DeleteTemplateOptions = { id: number; userId: number; + teamId?: number; }; -export const deleteTemplate = async ({ id, userId }: DeleteTemplateOptions) => { +export const deleteTemplate = async ({ id, userId, teamId }: DeleteTemplateOptions) => { return await prisma.template.delete({ where: { id, - OR: [ - { - userId, - }, - { - team: { - members: { - some: { + OR: + teamId === undefined + ? [ + { userId, + teamId: null, }, - }, - }, - }, - ], + ] + : [ + { + teamId, + team: { + members: { + some: { + userId, + }, + }, + }, + }, + ], }, }); }; diff --git a/packages/lib/server-only/template/get-template-by-id.ts b/packages/lib/server-only/template/get-template-by-id.ts index c4295c3c3..fbc8c48f8 100644 --- a/packages/lib/server-only/template/get-template-by-id.ts +++ b/packages/lib/server-only/template/get-template-by-id.ts @@ -1,34 +1,53 @@ import { prisma } from '@documenso/prisma'; import type { Prisma } from '@documenso/prisma/client'; +import { AppError, AppErrorCode } from '../../errors/app-error'; + export interface GetTemplateByIdOptions { id: number; userId: number; + teamId?: number; } -export const getTemplateById = async ({ id, userId }: GetTemplateByIdOptions) => { +export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOptions) => { const whereFilter: Prisma.TemplateWhereInput = { id, - OR: [ - { - userId, - }, - { - team: { - members: { - some: { + OR: + teamId === undefined + ? [ + { userId, + teamId: null, }, - }, - }, - }, - ], + ] + : [ + { + teamId, + team: { + members: { + some: { + userId, + }, + }, + }, + }, + ], }; - return await prisma.template.findFirstOrThrow({ + const template = await prisma.template.findFirst({ where: whereFilter, include: { + directLink: true, templateDocumentData: true, + templateMeta: true, + Recipient: true, + Field: true, }, }); + + if (!template) { + throw new AppError(AppErrorCode.NOT_FOUND, 'Template not found'); + } + + return template; }; diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts index f708ee2d7..6ef02f247 100644 --- a/packages/trpc/server/template-router/router.ts +++ b/packages/trpc/server/template-router/router.ts @@ -151,11 +151,11 @@ export const templateRouter = router({ .input(ZDeleteTemplateMutationSchema) .mutation(async ({ input, ctx }) => { try { - const { id } = input; + const { id, teamId } = input; const userId = ctx.user.id; - return await deleteTemplate({ userId, id }); + return await deleteTemplate({ userId, id, teamId }); } catch (err) { console.error(err); diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts index 36fde8453..5706eb7bd 100644 --- a/packages/trpc/server/template-router/schema.ts +++ b/packages/trpc/server/template-router/schema.ts @@ -60,6 +60,7 @@ export const ZToggleTemplateDirectLinkMutationSchema = z.object({ export const ZDeleteTemplateMutationSchema = z.object({ id: z.number().min(1), + teamId: z.number().optional(), }); export const ZUpdateTemplateSettingsMutationSchema = z.object({