diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx index a5dc9e23e..89d89f386 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -24,6 +24,8 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; import { Stepper } from '@documenso/ui/primitives/stepper'; import { useToast } from '@documenso/ui/primitives/use-toast'; +import { Comments } from '~/components/forms/comments'; + export type EditDocumentFormProps = { className?: string; user: User; @@ -179,60 +181,70 @@ export const EditDocumentForm = ({ const currentDocumentFlow = documentFlow[step]; return ( -
- - - +
+
+ + + + + + +
+ e.preventDefault()} + > + setStep(EditDocumentSteps[step - 1])} + > + + + + + + + +
+
+ + +

Comments

+
+ +
- -
- e.preventDefault()} - > - setStep(EditDocumentSteps[step - 1])} - > - - - - - - - -
); }; diff --git a/apps/web/src/components/comments/comment-card.tsx b/apps/web/src/components/comments/comment-card.tsx new file mode 100644 index 000000000..91485d251 --- /dev/null +++ b/apps/web/src/components/comments/comment-card.tsx @@ -0,0 +1,29 @@ +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; + +import { LocaleDate } from '~/components/formatter/locale-date'; + +export type CommentCardProps = { + comment: any; + className?: string; +}; + +export const CommentCard = ({ comment, className }: CommentCardProps) => { + return ( +
+

{comment.User.name}

+

+ +

+

{comment.comment}

+ +
+ ); +}; diff --git a/apps/web/src/components/forms/comments.tsx b/apps/web/src/components/forms/comments.tsx new file mode 100644 index 000000000..02807b5c7 --- /dev/null +++ b/apps/web/src/components/forms/comments.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { CornerDownRight } from 'lucide-react'; + +import { trpc } from '@documenso/trpc/react'; + +import { CommentCard } from '~/components/comments/comment-card'; + +export const Comments = () => { + const { data: comments } = trpc.comment.getComments.useQuery(); + + console.log(comments); + return ( +
+ {comments?.map((comment) => ( +
+ + {comment.replies && comment.replies.length > 0 ? ( +
+ {comment.replies.map((reply) => ( +
+ + +
+ ))} +
+ ) : null} +
+ ))} +
+ ); +}; diff --git a/apps/web/src/components/forms/profile.tsx b/apps/web/src/components/forms/profile.tsx index 0ce5c7f3d..793919c34 100644 --- a/apps/web/src/components/forms/profile.tsx +++ b/apps/web/src/components/forms/profile.tsx @@ -124,6 +124,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { className="h-44 w-full" containerClassName="rounded-lg border bg-background" defaultValue={user.signature ?? undefined} + onChange={(v) => onChange(v ?? '')} /> diff --git a/package.json b/package.json index 30076100f..ce02986e0 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "name": "@documenso/root", "workspaces": [ "apps/*", + "packages/*" ], "dependencies": {}, diff --git a/packages/lib/server-only/auth/api-auth-middleware.ts b/packages/lib/server-only/auth/api-auth-middleware.ts new file mode 100644 index 000000000..f452f2b28 --- /dev/null +++ b/packages/lib/server-only/auth/api-auth-middleware.ts @@ -0,0 +1,33 @@ +import { getUserByApiToken } from '@documenso/lib/server-only/public-api/get-user-by-token'; + +export type Headers = { + headers: { + authorization: string; + }; +}; + +export const authenticatedMiddleware = (fn: (args: T) => Promise) => { + return async (args: T) => { + if (!args.headers.authorization) { + return { + status: 401, + body: { + message: 'Unauthorized access', + }, + }; + } + + try { + await getUserByApiToken({ token: args.headers.authorization }); + } catch (err) { + return { + status: 401, + body: { + message: 'Unauthorized access', + }, + }; + } + + return fn(args); + }; +}; diff --git a/packages/lib/server-only/comment/find-comments.ts b/packages/lib/server-only/comment/find-comments.ts new file mode 100644 index 000000000..2d0aa6ea3 --- /dev/null +++ b/packages/lib/server-only/comment/find-comments.ts @@ -0,0 +1,25 @@ +import { prisma } from '@documenso/prisma'; + +export const findComments = async () => { + return await prisma.documentComment.findMany({ + where: { + parentId: null, + }, + include: { + User: { + select: { + name: true, + }, + }, + replies: { + include: { + User: { + select: { + name: true, + }, + }, + }, + }, + }, + }); +}; diff --git a/packages/prisma/migrations/20240109110727_document_comments/migration.sql b/packages/prisma/migrations/20240109110727_document_comments/migration.sql new file mode 100644 index 000000000..c182ffb46 --- /dev/null +++ b/packages/prisma/migrations/20240109110727_document_comments/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "DocumentComment" ( + "id" SERIAL NOT NULL, + "documentId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "comment" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "DocumentComment_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "DocumentComment" ADD CONSTRAINT "DocumentComment_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DocumentComment" ADD CONSTRAINT "DocumentComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20240110080456_add_comment_replies/migration.sql b/packages/prisma/migrations/20240110080456_add_comment_replies/migration.sql new file mode 100644 index 000000000..670fe4c76 --- /dev/null +++ b/packages/prisma/migrations/20240110080456_add_comment_replies/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "DocumentComment" ADD COLUMN "parentId" INTEGER; + +-- AddForeignKey +ALTER TABLE "DocumentComment" ADD CONSTRAINT "DocumentComment_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "DocumentComment"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index f0bfc6fda..7a85c4885 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -41,7 +41,8 @@ model User { twoFactorEnabled Boolean @default(false) twoFactorBackupCodes String? VerificationToken VerificationToken[] - Template Template[] + Template Template[] + DocumentComment DocumentComment[] @@index([email]) } @@ -121,27 +122,43 @@ enum DocumentStatus { } model Document { - id Int @id @default(autoincrement()) - userId Int - User User @relation(fields: [userId], references: [id], onDelete: Cascade) - title String - status DocumentStatus @default(DRAFT) - Recipient Recipient[] - Field Field[] - ShareLink DocumentShareLink[] - documentDataId String - documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade) - documentMeta DocumentMeta? - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - completedAt DateTime? - deletedAt DateTime? + id Int @id @default(autoincrement()) + userId Int + User User @relation(fields: [userId], references: [id], onDelete: Cascade) + title String + status DocumentStatus @default(DRAFT) + Recipient Recipient[] + Field Field[] + ShareLink DocumentShareLink[] + documentDataId String + documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade) + documentMeta DocumentMeta? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + completedAt DateTime? + deletedAt DateTime? + DocumentComments DocumentComment[] @@unique([documentDataId]) @@index([userId]) @@index([status]) } +model DocumentComment { + id Int @id @default(autoincrement()) + documentId Int + userId Int + comment String + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + parentId Int? + parent DocumentComment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: Cascade) + replies DocumentComment[] @relation("CommentReplies") + + Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) + User User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + enum DocumentDataType { S3_PATH BYTES @@ -161,8 +178,8 @@ model DocumentMeta { id String @id @default(cuid()) subject String? message String? - timezone String? @db.Text @default("Etc/UTC") - dateFormat String? @db.Text @default("yyyy-MM-dd hh:mm a") + timezone String? @default("Etc/UTC") @db.Text + dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text documentId Int @unique document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) } @@ -183,19 +200,19 @@ enum SigningStatus { } model Recipient { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) documentId Int? templateId Int? - email String @db.VarChar(255) - name String @default("") @db.VarChar(255) + email String @db.VarChar(255) + name String @default("") @db.VarChar(255) token String expired DateTime? signedAt DateTime? readStatus ReadStatus @default(NOT_OPENED) signingStatus SigningStatus @default(NOT_SIGNED) sendStatus SendStatus @default(NOT_SENT) - Document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) - Template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade) + Document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) + Template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade) Field Field[] Signature Signature[] @@ -279,10 +296,10 @@ model Template { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade) - User User @relation(fields: [userId], references: [id], onDelete: Cascade) - Recipient Recipient[] - Field Field[] + templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade) + User User @relation(fields: [userId], references: [id], onDelete: Cascade) + Recipient Recipient[] + Field Field[] @@unique([templateDocumentDataId]) } diff --git a/packages/trpc/server/comment-router/router.ts b/packages/trpc/server/comment-router/router.ts new file mode 100644 index 000000000..54aaa7825 --- /dev/null +++ b/packages/trpc/server/comment-router/router.ts @@ -0,0 +1,9 @@ +import { findComments } from '@documenso/lib/server-only/comment/find-comments'; + +import { procedure, router } from '../trpc'; + +export const commentRouter = router({ + getComments: procedure.query(async () => { + return await findComments(); + }), +}); diff --git a/packages/trpc/server/comment-router/schema.ts b/packages/trpc/server/comment-router/schema.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/trpc/server/router.ts b/packages/trpc/server/router.ts index 77d18e06d..e8531f8f6 100644 --- a/packages/trpc/server/router.ts +++ b/packages/trpc/server/router.ts @@ -1,5 +1,6 @@ import { adminRouter } from './admin-router/router'; import { authRouter } from './auth-router/router'; +import { commentRouter } from './comment-router/router'; import { documentRouter } from './document-router/router'; import { fieldRouter } from './field-router/router'; import { profileRouter } from './profile-router/router'; @@ -21,6 +22,7 @@ export const appRouter = router({ singleplayer: singleplayerRouter, twoFactorAuthentication: twoFactorAuthenticationRouter, template: templateRouter, + comment: commentRouter, }); export type AppRouter = typeof appRouter;