From f7d8ebb9deee8679ea2e6d2359d1fe0155c0c7b6 Mon Sep 17 00:00:00 2001 From: Nafees Nazik <84864519+G3root@users.noreply.github.com> Date: Thu, 16 Nov 2023 07:35:45 +0530 Subject: [PATCH] feat: enable resend email menu (#496) --- apps/web/src/app/(dashboard)/admin/layout.tsx | 2 +- .../app/(dashboard)/admin/users/[id]/page.tsx | 2 +- .../app/(dashboard)/documents/[id]/page.tsx | 2 +- .../_action-items/resend-document.tsx | 185 ++++++++++++++++++ .../documents/data-table-action-dropdown.tsx | 13 +- .../documents/duplicate-document-dialog.tsx | 10 +- .../src/app/(dashboard)/documents/page.tsx | 8 +- apps/web/src/app/(dashboard)/layout.tsx | 2 +- .../billing/create-billing-portal.action.ts | 5 +- .../billing/create-checkout.action.ts | 4 +- .../app/(dashboard)/settings/billing/page.tsx | 4 +- .../(dashboard)/settings/password/page.tsx | 2 +- .../app/(dashboard)/settings/profile/page.tsx | 2 +- .../src/app/(signing)/sign/[token]/layout.tsx | 2 +- .../src/app/(signing)/sign/[token]/page.tsx | 2 +- apps/web/src/app/not-found.tsx | 2 +- .../forms/edit-document/add-fields.action.ts | 4 +- .../forms/edit-document/add-signers.action.ts | 4 +- .../forms/edit-document/add-subject.action.ts | 4 +- .../next-auth/get-server-component-session.ts | 35 ++++ packages/lib/next-auth/get-server-session.ts | 30 +-- .../server-only/document/resend-document.tsx | 99 ++++++++++ .../lib/universal/upload/server-actions.ts | 2 +- .../trpc/server/document-router/router.ts | 23 +++ .../trpc/server/document-router/schema.ts | 5 + packages/ui/primitives/checkbox.tsx | 12 +- packages/ui/primitives/dialog.tsx | 16 +- 27 files changed, 405 insertions(+), 76 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/documents/_action-items/resend-document.tsx create mode 100644 packages/lib/next-auth/get-server-component-session.ts create mode 100644 packages/lib/server-only/document/resend-document.tsx diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx index 60066fc10..12330679d 100644 --- a/apps/web/src/app/(dashboard)/admin/layout.tsx +++ b/apps/web/src/app/(dashboard)/admin/layout.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { redirect } from 'next/navigation'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; import { AdminNav } from './nav'; diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx index 90ed0de4d..3baf5d63b 100644 --- a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; -import { z } from 'zod'; +import type { z } from 'zod'; import { trpc } from '@documenso/trpc/react'; import { ZUpdateProfileMutationByAdminSchema } from '@documenso/trpc/server/admin-router/schema'; diff --git a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx index 3493bcf30..08d5f61d3 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx @@ -3,7 +3,7 @@ import { redirect } from 'next/navigation'; import { ChevronLeft, Users2 } from 'lucide-react'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; diff --git a/apps/web/src/app/(dashboard)/documents/_action-items/resend-document.tsx b/apps/web/src/app/(dashboard)/documents/_action-items/resend-document.tsx new file mode 100644 index 000000000..056d6f3b0 --- /dev/null +++ b/apps/web/src/app/(dashboard)/documents/_action-items/resend-document.tsx @@ -0,0 +1,185 @@ +'use client'; + +import { useState } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { History } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import * as z from 'zod'; + +import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; +import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; +import { type Document, type Recipient, SigningStatus } from '@documenso/prisma/client'; +import { trpc as trpcReact } from '@documenso/trpc/react'; +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; +import { Checkbox } from '@documenso/ui/primitives/checkbox'; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { DropdownMenuItem } from '@documenso/ui/primitives/dropdown-menu'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, +} from '@documenso/ui/primitives/form/form'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { StackAvatar } from '~/components/(dashboard)/avatar/stack-avatar'; + +const FORM_ID = 'resend-email'; + +export type ResendDocumentActionItemProps = { + document: Document; + recipients: Recipient[]; +}; + +export const ZResendDocumentFormSchema = z.object({ + recipients: z.array(z.number()).min(1, { + message: 'You must select at least one item.', + }), +}); + +export type TResendDocumentFormSchema = z.infer; + +export const ResendDocumentActionItem = ({ + document, + recipients, +}: ResendDocumentActionItemProps) => { + const { toast } = useToast(); + + const [isOpen, setIsOpen] = useState(false); + + const isDisabled = + document.status !== 'PENDING' || + !recipients.some((r) => r.signingStatus === SigningStatus.NOT_SIGNED); + + const { mutateAsync: resendDocument } = trpcReact.document.resendDocument.useMutation(); + + const form = useForm({ + resolver: zodResolver(ZResendDocumentFormSchema), + defaultValues: { + recipients: [], + }, + }); + + const { + handleSubmit, + formState: { isSubmitting }, + } = form; + + const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => { + try { + await resendDocument({ documentId: document.id, recipients }); + + toast({ + title: 'Document re-sent', + description: 'Your document has been re-sent successfully.', + duration: 5000, + }); + + setIsOpen(false); + } catch (err) { + toast({ + title: 'Something went wrong', + description: 'This document could not be re-sent at this time. Please try again.', + variant: 'destructive', + duration: 7500, + }); + } + }; + + return ( + <> + + + e.preventDefault()}> + + Resend + + + + + + +

Who do you want to remind?

+
+
+ +
+ + ( + <> + {recipients.map((recipient) => ( + + + + {recipient.email} + + + + + checked + ? onChange([...value, recipient.id]) + : onChange(value.filter((v) => v !== recipient.id)) + } + /> + + + ))} + + )} + /> + + + + +
+ + + + + +
+
+
+
+ + ); +}; 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 c12d329ab..d1a7dcb5f 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 @@ -8,7 +8,6 @@ import { Copy, Download, Edit, - History, Loader, MoreHorizontal, Pencil, @@ -19,8 +18,9 @@ import { import { useSession } from 'next-auth/react'; import { getFile } from '@documenso/lib/universal/upload/get-file'; -import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client'; -import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; +import type { Document, Recipient, User } from '@documenso/prisma/client'; +import { DocumentStatus } 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 { @@ -31,6 +31,7 @@ import { DropdownMenuTrigger, } from '@documenso/ui/primitives/dropdown-menu'; +import { ResendDocumentActionItem } from './_action-items/resend-document'; import { DeleteDraftDocumentDialog } from './delete-draft-document-dialog'; import { DuplicateDocumentDialog } from './duplicate-document-dialog'; @@ -96,6 +97,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = window.URL.revokeObjectURL(link.href); }; + const nonSignedRecipients = row.Recipient.filter((item) => item.signingStatus !== 'SIGNED'); return ( @@ -141,10 +143,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = Share - - - Resend - + ) : (
- +
)} diff --git a/apps/web/src/app/(dashboard)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx index a5ab6a54e..4323be571 100644 --- a/apps/web/src/app/(dashboard)/documents/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/page.tsx @@ -1,6 +1,6 @@ import Link from 'next/link'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { findDocuments } from '@documenso/lib/server-only/document/find-documents'; import { getStats } from '@documenso/lib/server-only/document/get-stats'; import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status'; @@ -8,10 +8,8 @@ import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-documen import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector'; -import { - PeriodSelectorValue, - isPeriodSelectorValue, -} from '~/components/(dashboard)/period-selector/types'; +import type { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/types'; +import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types'; import { DocumentStatus } from '~/components/formatter/document-status'; import { DocumentsDataTable } from './data-table'; diff --git a/apps/web/src/app/(dashboard)/layout.tsx b/apps/web/src/app/(dashboard)/layout.tsx index efd3aa2ea..a65211b2e 100644 --- a/apps/web/src/app/(dashboard)/layout.tsx +++ b/apps/web/src/app/(dashboard)/layout.tsx @@ -6,7 +6,7 @@ import { getServerSession } from 'next-auth'; import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/server'; import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { Header } from '~/components/(dashboard)/layout/header'; import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus'; diff --git a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts index cef36ee3f..ee5dbf175 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts +++ b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts @@ -5,8 +5,9 @@ import { getStripeCustomerById, } from '@documenso/ee/server-only/stripe/get-customer'; import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; -import { Stripe, stripe } from '@documenso/lib/server-only/stripe'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; +import type { Stripe } from '@documenso/lib/server-only/stripe'; +import { stripe } from '@documenso/lib/server-only/stripe'; import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id'; export const createBillingPortal = async () => { diff --git a/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts b/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts index 2f07c37dd..0552c55ec 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts +++ b/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts @@ -7,8 +7,8 @@ import { getStripeCustomerById, } from '@documenso/ee/server-only/stripe/get-customer'; import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; -import { Stripe } from '@documenso/lib/server-only/stripe'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; +import type { Stripe } from '@documenso/lib/server-only/stripe'; import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id'; export type CreateCheckoutOptions = { diff --git a/apps/web/src/app/(dashboard)/settings/billing/page.tsx b/apps/web/src/app/(dashboard)/settings/billing/page.tsx index 9f7e44e25..c7161f4ae 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/billing/page.tsx @@ -4,9 +4,9 @@ import { match } from 'ts-pattern'; import { getPricesByInterval } from '@documenso/ee/server-only/stripe/get-prices-by-interval'; import { getProductByPriceId } from '@documenso/ee/server-only/stripe/get-product-by-price-id'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag'; -import { Stripe } from '@documenso/lib/server-only/stripe'; +import type { Stripe } from '@documenso/lib/server-only/stripe'; import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id'; import { LocaleDate } from '~/components/formatter/locale-date'; diff --git a/apps/web/src/app/(dashboard)/settings/password/page.tsx b/apps/web/src/app/(dashboard)/settings/password/page.tsx index 90fcbe25d..701335180 100644 --- a/apps/web/src/app/(dashboard)/settings/password/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/password/page.tsx @@ -1,4 +1,4 @@ -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { PasswordForm } from '~/components/forms/password'; diff --git a/apps/web/src/app/(dashboard)/settings/profile/page.tsx b/apps/web/src/app/(dashboard)/settings/profile/page.tsx index 716f3c39c..b577ec93e 100644 --- a/apps/web/src/app/(dashboard)/settings/profile/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/profile/page.tsx @@ -1,4 +1,4 @@ -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { ProfileForm } from '~/components/forms/profile'; diff --git a/apps/web/src/app/(signing)/sign/[token]/layout.tsx b/apps/web/src/app/(signing)/sign/[token]/layout.tsx index a25c16c0d..cfec41cdf 100644 --- a/apps/web/src/app/(signing)/sign/[token]/layout.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/layout.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { Header as AuthenticatedHeader } from '~/components/(dashboard)/layout/header'; import { NextAuthProvider } from '~/providers/next-auth'; diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index a1ad0b170..67e679412 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -3,7 +3,7 @@ import { notFound, redirect } from 'next/navigation'; import { match } from 'ts-pattern'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; -import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document'; import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx index f580655af..76017c121 100644 --- a/apps/web/src/app/not-found.tsx +++ b/apps/web/src/app/not-found.tsx @@ -1,6 +1,6 @@ import Link from 'next/link'; -import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { Button } from '@documenso/ui/primitives/button'; import NotFoundPartial from '~/components/partials/not-found'; diff --git a/apps/web/src/components/forms/edit-document/add-fields.action.ts b/apps/web/src/components/forms/edit-document/add-fields.action.ts index c07758b9f..edc5e7e39 100644 --- a/apps/web/src/components/forms/edit-document/add-fields.action.ts +++ b/apps/web/src/components/forms/edit-document/add-fields.action.ts @@ -1,8 +1,8 @@ 'use server'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document'; -import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types'; +import type { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types'; export type AddFieldsActionInput = TAddFieldsFormSchema & { documentId: number; diff --git a/apps/web/src/components/forms/edit-document/add-signers.action.ts b/apps/web/src/components/forms/edit-document/add-signers.action.ts index 05151498a..c36d51c41 100644 --- a/apps/web/src/components/forms/edit-document/add-signers.action.ts +++ b/apps/web/src/components/forms/edit-document/add-signers.action.ts @@ -1,8 +1,8 @@ 'use server'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document'; -import { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types'; +import type { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types'; export type AddSignersActionInput = TAddSignersFormSchema & { documentId: number; diff --git a/apps/web/src/components/forms/edit-document/add-subject.action.ts b/apps/web/src/components/forms/edit-document/add-subject.action.ts index 8fe37fecc..56d6f694d 100644 --- a/apps/web/src/components/forms/edit-document/add-subject.action.ts +++ b/apps/web/src/components/forms/edit-document/add-subject.action.ts @@ -1,9 +1,9 @@ 'use server'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta'; import { sendDocument } from '@documenso/lib/server-only/document/send-document'; -import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types'; +import type { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types'; export type CompleteDocumentActionInput = TAddSubjectFormSchema & { documentId: number; diff --git a/packages/lib/next-auth/get-server-component-session.ts b/packages/lib/next-auth/get-server-component-session.ts new file mode 100644 index 000000000..7e35af5ad --- /dev/null +++ b/packages/lib/next-auth/get-server-component-session.ts @@ -0,0 +1,35 @@ +'use server'; + +import { cache } from 'react'; + +import { getServerSession as getNextAuthServerSession } from 'next-auth'; + +import { prisma } from '@documenso/prisma'; + +import { NEXT_AUTH_OPTIONS } from './auth-options'; + +export const getServerComponentSession = cache(async () => { + const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS); + + if (!session || !session.user?.email) { + return { user: null, session: null }; + } + + const user = await prisma.user.findFirstOrThrow({ + where: { + email: session.user.email, + }, + }); + + return { user, session }; +}); + +export const getRequiredServerComponentSession = cache(async () => { + const { user, session } = await getServerComponentSession(); + + if (!user || !session) { + throw new Error('No session found'); + } + + return { user, session }; +}); diff --git a/packages/lib/next-auth/get-server-session.ts b/packages/lib/next-auth/get-server-session.ts index 6a61ed8fd..215f754ac 100644 --- a/packages/lib/next-auth/get-server-session.ts +++ b/packages/lib/next-auth/get-server-session.ts @@ -1,6 +1,6 @@ -import { cache } from 'react'; +'use server'; -import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from 'next'; +import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from 'next'; import { getServerSession as getNextAuthServerSession } from 'next-auth'; @@ -28,29 +28,3 @@ export const getServerSession = async ({ req, res }: GetServerSessionOptions) => return { user, session }; }; - -export const getServerComponentSession = cache(async () => { - const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS); - - if (!session || !session.user?.email) { - return { user: null, session: null }; - } - - const user = await prisma.user.findFirstOrThrow({ - where: { - email: session.user.email, - }, - }); - - return { user, session }; -}); - -export const getRequiredServerComponentSession = cache(async () => { - const { user, session } = await getServerComponentSession(); - - if (!user || !session) { - throw new Error('No session found'); - } - - return { user, session }; -}); diff --git a/packages/lib/server-only/document/resend-document.tsx b/packages/lib/server-only/document/resend-document.tsx new file mode 100644 index 000000000..3069fc0ac --- /dev/null +++ b/packages/lib/server-only/document/resend-document.tsx @@ -0,0 +1,99 @@ +import { createElement } from 'react'; + +import { mailer } from '@documenso/email/mailer'; +import { render } from '@documenso/email/render'; +import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite'; +import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email'; +import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template'; +import { prisma } from '@documenso/prisma'; +import { DocumentStatus, SigningStatus } from '@documenso/prisma/client'; + +export type ResendDocumentOptions = { + documentId: number; + userId: number; + recipients: number[]; +}; + +export const resendDocument = async ({ documentId, userId, recipients }: ResendDocumentOptions) => { + const user = await prisma.user.findFirstOrThrow({ + where: { + id: userId, + }, + }); + + const document = await prisma.document.findUnique({ + where: { + id: documentId, + userId, + }, + include: { + Recipient: { + where: { + id: { + in: recipients, + }, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }, + documentMeta: true, + }, + }); + + const customEmail = document?.documentMeta; + + if (!document) { + throw new Error('Document not found'); + } + + if (document.Recipient.length === 0) { + throw new Error('Document has no recipients'); + } + + if (document.status === DocumentStatus.DRAFT) { + throw new Error('Can not send draft document'); + } + + if (document.status === DocumentStatus.COMPLETED) { + throw new Error('Can not send completed document'); + } + + await Promise.all([ + document.Recipient.map(async (recipient) => { + const { email, name } = recipient; + + const customEmailTemplate = { + 'signer.name': name, + 'signer.email': email, + 'document.name': document.title, + }; + + const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; + const signDocumentLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`; + + const template = createElement(DocumentInviteEmailTemplate, { + documentName: document.title, + inviterName: user.name || undefined, + inviterEmail: user.email, + assetBaseUrl, + signDocumentLink, + customBody: renderCustomEmailTemplate(customEmail?.message || '', customEmailTemplate), + }); + + await mailer.sendMail({ + to: { + address: email, + name, + }, + from: { + name: FROM_NAME, + address: FROM_ADDRESS, + }, + subject: customEmail?.subject + ? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate) + : 'Please sign this document', + html: render(template), + text: render(template, { plainText: true }), + }); + }), + ]); +}; diff --git a/packages/lib/universal/upload/server-actions.ts b/packages/lib/universal/upload/server-actions.ts index a2122aae2..ec0bde59a 100644 --- a/packages/lib/universal/upload/server-actions.ts +++ b/packages/lib/universal/upload/server-actions.ts @@ -10,7 +10,7 @@ import slugify from '@sindresorhus/slugify'; import path from 'node:path'; import { ONE_HOUR, ONE_SECOND } from '../../constants/time'; -import { getServerComponentSession } from '../../next-auth/get-server-session'; +import { getServerComponentSession } from '../../next-auth/get-server-component-session'; import { alphaid } from '../id'; export const getPresignPostUrl = async (fileName: string, contentType: string) => { diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index e43d67c99..bd92312da 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -6,6 +6,7 @@ import { deleteDraftDocument } from '@documenso/lib/server-only/document/delete- 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 { resendDocument } from '@documenso/lib/server-only/document/resend-document'; import { sendDocument } from '@documenso/lib/server-only/document/send-document'; import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document'; import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document'; @@ -16,6 +17,7 @@ import { ZDeleteDraftDocumentMutationSchema, ZGetDocumentByIdQuerySchema, ZGetDocumentByTokenQuerySchema, + ZResendDocumentMutationSchema, ZSendDocumentMutationSchema, ZSetFieldsForDocumentMutationSchema, ZSetRecipientsForDocumentMutationSchema, @@ -174,6 +176,27 @@ export const documentRouter = router({ } }), + resendDocument: authenticatedProcedure + .input(ZResendDocumentMutationSchema) + .mutation(async ({ input, ctx }) => { + try { + const { documentId, recipients } = input; + + return await resendDocument({ + userId: ctx.user.id, + documentId, + recipients, + }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to resend this document. Please try again later.', + }); + } + }), + duplicateDocument: authenticatedProcedure .input(ZGetDocumentByIdQuerySchema) .mutation(async ({ input, ctx }) => { diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index e5b27c0ea..b9bea71c3 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -60,6 +60,11 @@ export const ZSendDocumentMutationSchema = z.object({ documentId: z.number(), }); +export const ZResendDocumentMutationSchema = z.object({ + documentId: z.number(), + recipients: z.array(z.number()).min(1), +}); + export type TSendDocumentMutationSchema = z.infer; export const ZDeleteDraftDocumentMutationSchema = z.object({ diff --git a/packages/ui/primitives/checkbox.tsx b/packages/ui/primitives/checkbox.tsx index 63a945c63..5acf35f9d 100644 --- a/packages/ui/primitives/checkbox.tsx +++ b/packages/ui/primitives/checkbox.tsx @@ -9,8 +9,10 @@ import { cn } from '../lib/utils'; const Checkbox = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { + checkClassName?: string; + } +>(({ className, checkClassName, ...props }, ref) => ( - - + + )); diff --git a/packages/ui/primitives/dialog.tsx b/packages/ui/primitives/dialog.tsx index eabb7ff35..f75e9cdec 100644 --- a/packages/ui/primitives/dialog.tsx +++ b/packages/ui/primitives/dialog.tsx @@ -11,6 +11,8 @@ const Dialog = DialogPrimitive.Root; const DialogTrigger = DialogPrimitive.Trigger; +const DialogClose = DialogPrimitive.Close; + const DialogPortal = ({ children, position = 'start', @@ -51,8 +53,9 @@ const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { position?: 'start' | 'end' | 'center'; + hideClose?: boolean; } ->(({ className, children, position = 'start', ...props }, ref) => ( +>(({ className, children, position = 'start', hideClose = false, ...props }, ref) => ( {children} - - - Close - + {!hideClose && ( + + + Close + + )} )); @@ -125,4 +130,5 @@ export { DialogTitle, DialogDescription, DialogPortal, + DialogClose, };