Compare commits

..

6 Commits

Author SHA1 Message Date
Catalin Pit 13d23b6111 feat: doc comments 2024-01-11 11:53:52 +02:00
Lucas Smith b09071ebc7 feat: jump to next field (#805)
When the fields are not filled, the button will say "Next field". Clicking on the button takes you to
the unfilled field.
2024-01-10 15:27:18 +11:00
Timur Ercan 66bb56047a chore: update roadmap links 2024-01-09 14:32:49 +01:00
Catalin Pit 3054d84ba7 chore: implemented feedback 2024-01-08 09:58:34 +02:00
Catalin Pit 4fd6a0d5b6 chore: update onOpenChange 2024-01-05 13:06:16 +02:00
Catalin Pit fface15a22 feat: jump to next field 2024-01-05 12:56:07 +02:00
15 changed files with 273 additions and 82 deletions
@@ -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 (
<div className={cn('grid w-full grid-cols-12 gap-8', className)}>
<Card
className="relative col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7"
gradient
>
<CardContent className="p-2">
<LazyPDFViewer key={documentData.id} documentData={documentData} />
<div>
<div className={cn('grid w-full grid-cols-12 gap-8', className)}>
<Card
className="relative col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7"
gradient
>
<CardContent className="p-2">
<LazyPDFViewer key={documentData.id} documentData={documentData} />
</CardContent>
</Card>
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
<DocumentFlowFormContainer
className="lg:h-[calc(100vh-6rem)]"
onSubmit={(e) => e.preventDefault()}
>
<Stepper
currentStep={currentDocumentFlow.stepIndex}
setCurrentStep={(step) => setStep(EditDocumentSteps[step - 1])}
>
<AddTitleFormPartial
key={recipients.length}
documentFlow={documentFlow.title}
recipients={recipients}
fields={fields}
document={document}
onSubmit={onAddTitleFormSubmit}
/>
<AddSignersFormPartial
key={recipients.length}
documentFlow={documentFlow.signers}
document={document}
recipients={recipients}
fields={fields}
onSubmit={onAddSignersFormSubmit}
/>
<AddFieldsFormPartial
key={fields.length}
documentFlow={documentFlow.fields}
recipients={recipients}
fields={fields}
onSubmit={onAddFieldsFormSubmit}
/>
<AddSubjectFormPartial
key={recipients.length}
documentFlow={documentFlow.subject}
document={document}
recipients={recipients}
fields={fields}
onSubmit={onAddSubjectFormSubmit}
/>
</Stepper>
</DocumentFlowFormContainer>
</div>
</div>
<Card className="my-8" gradient={true} degrees={200}>
<CardContent className="mt-8 flex flex-col">
<h2 className="text-foreground text-2xl font-semibold">Comments</h2>
<hr className="border-border mb-4 mt-4" />
<Comments />
<hr className="border-border -mt-4 mb-4" />
</CardContent>
</Card>
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
<DocumentFlowFormContainer
className="lg:h-[calc(100vh-6rem)]"
onSubmit={(e) => e.preventDefault()}
>
<Stepper
currentStep={currentDocumentFlow.stepIndex}
setCurrentStep={(step) => setStep(EditDocumentSteps[step - 1])}
>
<AddTitleFormPartial
key={recipients.length}
documentFlow={documentFlow.title}
recipients={recipients}
fields={fields}
document={document}
onSubmit={onAddTitleFormSubmit}
/>
<AddSignersFormPartial
key={recipients.length}
documentFlow={documentFlow.signers}
document={document}
recipients={recipients}
fields={fields}
onSubmit={onAddSignersFormSubmit}
/>
<AddFieldsFormPartial
key={fields.length}
documentFlow={documentFlow.fields}
recipients={recipients}
fields={fields}
onSubmit={onAddFieldsFormSubmit}
/>
<AddSubjectFormPartial
key={recipients.length}
documentFlow={documentFlow.subject}
document={document}
recipients={recipients}
fields={fields}
onSubmit={onAddSubjectFormSubmit}
/>
</Stepper>
</DocumentFlowFormContainer>
</div>
</div>
);
};
@@ -49,6 +49,11 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
return sortFieldsByPosition(fields.filter((field) => !field.inserted));
}, [fields]);
const fieldsValidated = () => {
setValidateUninsertedFields(true);
validateFieldsInserted(fields);
};
const onFormSubmit = async () => {
setValidateUninsertedFields(true);
@@ -154,6 +159,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
onSignatureComplete={handleSubmit(onFormSubmit)}
document={document}
fields={fields}
fieldsValidated={fieldsValidated}
/>
</div>
</div>
@@ -15,6 +15,7 @@ export type SignDialogProps = {
isSubmitting: boolean;
document: Document;
fields: Field[];
fieldsValidated: () => void | Promise<void>;
onSignatureComplete: () => void | Promise<void>;
};
@@ -22,6 +23,7 @@ export const SignDialog = ({
isSubmitting,
document,
fields,
fieldsValidated,
onSignatureComplete,
}: SignDialogProps) => {
const [showDialog, setShowDialog] = useState(false);
@@ -29,16 +31,16 @@ export const SignDialog = ({
const isComplete = fields.every((field) => field.inserted);
return (
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<Dialog open={showDialog && isComplete} onOpenChange={setShowDialog}>
<DialogTrigger asChild>
<Button
className="w-full"
type="button"
size="lg"
disabled={!isComplete}
onClick={fieldsValidated}
loading={isSubmitting}
>
Complete
{isComplete ? 'Complete' : 'Next field'}
</Button>
</DialogTrigger>
<DialogContent>
@@ -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 (
<div className={cn('mb-8', className)} key={comment.id}>
<p className="font-semibold">{comment.User.name}</p>
<p className="text-sm">
<LocaleDate
date={comment.createdAt}
format={{
month: 'long',
day: 'numeric',
year: 'numeric',
}}
/>
</p>
<p className="mb-2 mt-2 text-base">{comment.comment}</p>
<Button>Reply</Button>
</div>
);
};
@@ -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 (
<div>
{comments?.map((comment) => (
<div key={comment.id}>
<CommentCard comment={comment} className="mb-8" />
{comment.replies && comment.replies.length > 0 ? (
<div>
{comment.replies.map((reply) => (
<div className="ml-6 flex" key={reply.id}>
<CornerDownRight className="flex shrink-0" />
<CommentCard comment={reply} className="ml-6" />
</div>
))}
</div>
) : null}
</div>
))}
</div>
);
};
@@ -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 ?? '')}
/>
</FormControl>
+1
View File
@@ -45,6 +45,7 @@
"name": "@documenso/root",
"workspaces": [
"apps/*",
"packages/*"
],
"dependencies": {},
@@ -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 = <T extends Headers>(fn: (args: T) => Promise<any>) => {
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);
};
};
@@ -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,
},
},
},
},
},
});
};
@@ -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;
@@ -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;
+44 -27
View File
@@ -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])
}
@@ -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();
}),
});
+2
View File
@@ -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;