diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx index 3937cd93d..c12d329ab 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx @@ -32,6 +32,7 @@ import { } from '@documenso/ui/primitives/dropdown-menu'; import { DeleteDraftDocumentDialog } from './delete-draft-document-dialog'; +import { DuplicateDocumentDialog } from './duplicate-document-dialog'; export type DataTableActionDropdownProps = { row: Document & { @@ -44,6 +45,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = const { data: session } = useSession(); const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false); if (!session) { return null; @@ -122,7 +124,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = Download - + setDuplicateDialogOpen(true)}> Duplicate @@ -165,6 +167,13 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = onOpenChange={setDeleteDialogOpen} /> )} + {isDuplicateDialogOpen && ( + + )} ); }; diff --git a/apps/web/src/app/(dashboard)/documents/duplicate-document-dialog.tsx b/apps/web/src/app/(dashboard)/documents/duplicate-document-dialog.tsx new file mode 100644 index 000000000..a63141323 --- /dev/null +++ b/apps/web/src/app/(dashboard)/documents/duplicate-document-dialog.tsx @@ -0,0 +1,101 @@ +import { useRouter } from 'next/navigation'; + +import { trpc as trpcReact } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@documenso/ui/primitives/dialog'; +import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +type DuplicateDocumentDialogProps = { + id: number; + open: boolean; + onOpenChange: (_open: boolean) => void; +}; + +export const DuplicateDocumentDialog = ({ + id, + open, + onOpenChange, +}: DuplicateDocumentDialogProps) => { + const router = useRouter(); + const { toast } = useToast(); + const { data, isLoading } = trpcReact.document.getDocumentById.useQuery({ + id, + }); + const { mutateAsync: duplicateDocument, isLoading: isDuplicateLoading } = + trpcReact.document.duplicateDocument.useMutation({ + onSuccess: (newId) => { + router.push(`/documents/${newId}`); + toast({ + title: 'Document Duplicated', + description: 'Your document has been successfully duplicated.', + duration: 5000, + }); + + onOpenChange(false); + }, + }); + + const onDuplicate = async () => { + try { + await duplicateDocument({ id }); + } catch { + toast({ + title: 'Something went wrong', + description: 'This document could not be duplicated at this time. Please try again.', + variant: 'destructive', + duration: 7500, + }); + } + }; + + return ( + !isLoading && onOpenChange(value)}> + + + Duplicate + + {!data?.documentData || isLoading ? ( +
+

+ Loading Document... +

+
+ ) : ( +
+ +
+ )} + + +
+ + + +
+
+
+
+ ); +}; diff --git a/packages/lib/server-only/document/duplicate-document-by-id.ts b/packages/lib/server-only/document/duplicate-document-by-id.ts new file mode 100644 index 000000000..5d3bb9f9c --- /dev/null +++ b/packages/lib/server-only/document/duplicate-document-by-id.ts @@ -0,0 +1,56 @@ +import { prisma } from '@documenso/prisma'; + +export interface DuplicateDocumentByIdOptions { + id: number; + userId: number; +} + +export const duplicateDocumentById = async ({ id, userId }: DuplicateDocumentByIdOptions) => { + const document = await prisma.document.findUniqueOrThrow({ + where: { + id, + userId: userId, + }, + select: { + title: true, + userId: true, + documentData: { + select: { + data: true, + initialData: true, + type: true, + }, + }, + documentMeta: { + select: { + message: true, + subject: true, + }, + }, + }, + }); + + const createdDocument = await prisma.document.create({ + data: { + title: document.title, + User: { + connect: { + id: document.userId, + }, + }, + documentData: { + create: { + ...document.documentData, + data: document.documentData.initialData, + }, + }, + documentMeta: { + create: { + ...document.documentMeta, + }, + }, + }, + }); + + return createdDocument.id; +}; diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index d8e165594..e43d67c99 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -3,6 +3,7 @@ import { TRPCError } from '@trpc/server'; import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { createDocument } from '@documenso/lib/server-only/document/create-document'; import { deleteDraftDocument } from '@documenso/lib/server-only/document/delete-draft-document'; +import { duplicateDocumentById } from '@documenso/lib/server-only/document/duplicate-document-by-id'; import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; import { sendDocument } from '@documenso/lib/server-only/document/send-document'; @@ -172,4 +173,23 @@ export const documentRouter = router({ }); } }), + + duplicateDocument: authenticatedProcedure + .input(ZGetDocumentByIdQuerySchema) + .mutation(async ({ input, ctx }) => { + try { + const { id } = input; + + return await duplicateDocumentById({ + id, + userId: ctx.user.id, + }); + } catch (err) { + console.log(err); + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We are unable to duplicate this document. Please try again later.', + }); + } + }), });