mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 16:23:06 +10:00
feat: enable resend email menu (#496)
This commit is contained in:
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { redirect } from 'next/navigation';
|
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 { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
||||||
|
|
||||||
import { AdminNav } from './nav';
|
import { AdminNav } from './nav';
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useRouter } from 'next/navigation';
|
|||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { ZUpdateProfileMutationByAdminSchema } from '@documenso/trpc/server/admin-router/schema';
|
import { ZUpdateProfileMutationByAdminSchema } from '@documenso/trpc/server/admin-router/schema';
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { redirect } from 'next/navigation';
|
|||||||
|
|
||||||
import { ChevronLeft, Users2 } from 'lucide-react';
|
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 { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
||||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||||
|
|||||||
@ -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<typeof ZResendDocumentFormSchema>;
|
||||||
|
|
||||||
|
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<TResendDocumentFormSchema>({
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem disabled={isDisabled} onSelect={(e) => e.preventDefault()}>
|
||||||
|
<History className="mr-2 h-4 w-4" />
|
||||||
|
Resend
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent className="sm:max-w-sm" hideClose>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<h1 className="text-center text-xl">Who do you want to remind?</h1>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form id={FORM_ID} onSubmit={handleSubmit(onFormSubmit)} className="px-3">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="recipients"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<>
|
||||||
|
{recipients.map((recipient) => (
|
||||||
|
<FormItem
|
||||||
|
key={recipient.id}
|
||||||
|
className="flex flex-row items-center justify-between gap-x-3"
|
||||||
|
>
|
||||||
|
<FormLabel
|
||||||
|
className={cn('my-2 flex items-center gap-2 font-normal', {
|
||||||
|
'opacity-50': !value.includes(recipient.id),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<StackAvatar
|
||||||
|
key={recipient.id}
|
||||||
|
type={getRecipientType(recipient)}
|
||||||
|
fallbackText={recipientAbbreviation(recipient)}
|
||||||
|
/>
|
||||||
|
{recipient.email}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
className="h-5 w-5 rounded-full data-[state=checked]:border-black data-[state=checked]:bg-black "
|
||||||
|
checkClassName="text-white"
|
||||||
|
value={recipient.id}
|
||||||
|
checked={value.includes(recipient.id)}
|
||||||
|
onCheckedChange={(checked: boolean) =>
|
||||||
|
checked
|
||||||
|
? onChange([...value, recipient.id])
|
||||||
|
: onChange(value.filter((v) => v !== recipient.id))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
||||||
|
variant="secondary"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
|
||||||
|
<Button className="flex-1" loading={isSubmitting} type="submit" form={FORM_ID}>
|
||||||
|
Send reminder
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -8,7 +8,6 @@ import {
|
|||||||
Copy,
|
Copy,
|
||||||
Download,
|
Download,
|
||||||
Edit,
|
Edit,
|
||||||
History,
|
|
||||||
Loader,
|
Loader,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Pencil,
|
Pencil,
|
||||||
@ -19,8 +18,9 @@ import {
|
|||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||||
import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client';
|
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
||||||
import { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
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 { trpc as trpcClient } from '@documenso/trpc/client';
|
||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||||
import {
|
import {
|
||||||
@ -31,6 +31,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@documenso/ui/primitives/dropdown-menu';
|
} from '@documenso/ui/primitives/dropdown-menu';
|
||||||
|
|
||||||
|
import { ResendDocumentActionItem } from './_action-items/resend-document';
|
||||||
import { DeleteDraftDocumentDialog } from './delete-draft-document-dialog';
|
import { DeleteDraftDocumentDialog } from './delete-draft-document-dialog';
|
||||||
import { DuplicateDocumentDialog } from './duplicate-document-dialog';
|
import { DuplicateDocumentDialog } from './duplicate-document-dialog';
|
||||||
|
|
||||||
@ -96,6 +97,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
window.URL.revokeObjectURL(link.href);
|
window.URL.revokeObjectURL(link.href);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const nonSignedRecipients = row.Recipient.filter((item) => item.signingStatus !== 'SIGNED');
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
@ -141,10 +143,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
|
|
||||||
<DropdownMenuLabel>Share</DropdownMenuLabel>
|
<DropdownMenuLabel>Share</DropdownMenuLabel>
|
||||||
|
|
||||||
<DropdownMenuItem disabled>
|
<ResendDocumentActionItem document={row} recipients={nonSignedRecipients} />
|
||||||
<History className="mr-2 h-4 w-4" />
|
|
||||||
Resend
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DocumentShareButton
|
<DocumentShareButton
|
||||||
documentId={row.id}
|
documentId={row.id}
|
||||||
|
|||||||
@ -26,14 +26,14 @@ export const DuplicateDocumentDialog = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { data, isLoading } = trpcReact.document.getDocumentById.useQuery({
|
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentData = data?.documentData
|
const documentData = document?.documentData
|
||||||
? {
|
? {
|
||||||
...data.documentData,
|
...document.documentData,
|
||||||
data: data.documentData.initialData,
|
data: document.documentData.initialData,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ export const DuplicateDocumentDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-2 [&>div]:h-[50vh] [&>div]:overflow-y-scroll ">
|
<div className="p-2 [&>div]:h-[50vh] [&>div]:overflow-y-scroll ">
|
||||||
<LazyPDFViewer key={data?.id} documentData={documentData} />
|
<LazyPDFViewer key={document?.id} documentData={documentData} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import Link from 'next/link';
|
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 { findDocuments } from '@documenso/lib/server-only/document/find-documents';
|
||||||
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
||||||
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
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 { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
|
|
||||||
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
|
||||||
import {
|
import type { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
|
||||||
PeriodSelectorValue,
|
import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
|
||||||
isPeriodSelectorValue,
|
|
||||||
} from '~/components/(dashboard)/period-selector/types';
|
|
||||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||||
|
|
||||||
import { DocumentsDataTable } from './data-table';
|
import { DocumentsDataTable } from './data-table';
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { getServerSession } from 'next-auth';
|
|||||||
|
|
||||||
import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/server';
|
import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/server';
|
||||||
import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options';
|
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 { Header } from '~/components/(dashboard)/layout/header';
|
||||||
import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
|
import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
|
||||||
|
|||||||
@ -5,8 +5,9 @@ import {
|
|||||||
getStripeCustomerById,
|
getStripeCustomerById,
|
||||||
} from '@documenso/ee/server-only/stripe/get-customer';
|
} from '@documenso/ee/server-only/stripe/get-customer';
|
||||||
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
|
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';
|
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
||||||
|
|
||||||
export const createBillingPortal = async () => {
|
export const createBillingPortal = async () => {
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import {
|
|||||||
getStripeCustomerById,
|
getStripeCustomerById,
|
||||||
} from '@documenso/ee/server-only/stripe/get-customer';
|
} from '@documenso/ee/server-only/stripe/get-customer';
|
||||||
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
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 { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
||||||
|
|
||||||
export type CreateCheckoutOptions = {
|
export type CreateCheckoutOptions = {
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import { match } from 'ts-pattern';
|
|||||||
|
|
||||||
import { getPricesByInterval } from '@documenso/ee/server-only/stripe/get-prices-by-interval';
|
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 { 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 { 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 { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||||
|
|||||||
@ -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';
|
import { PasswordForm } from '~/components/forms/password';
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
import { ProfileForm } from '~/components/forms/profile';
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
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 { Header as AuthenticatedHeader } from '~/components/(dashboard)/layout/header';
|
||||||
import { NextAuthProvider } from '~/providers/next-auth';
|
import { NextAuthProvider } from '~/providers/next-auth';
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { notFound, redirect } from 'next/navigation';
|
|||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
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 { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import Link from 'next/link';
|
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 { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
import NotFoundPartial from '~/components/partials/not-found';
|
import NotFoundPartial from '~/components/partials/not-found';
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
'use server';
|
'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 { 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 & {
|
export type AddFieldsActionInput = TAddFieldsFormSchema & {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
'use server';
|
'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 { 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 & {
|
export type AddSignersActionInput = TAddSignersFormSchema & {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
'use server';
|
'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 { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
||||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
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 & {
|
export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
|
|||||||
35
packages/lib/next-auth/get-server-component-session.ts
Normal file
35
packages/lib/next-auth/get-server-component-session.ts
Normal file
@ -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 };
|
||||||
|
});
|
||||||
@ -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';
|
import { getServerSession as getNextAuthServerSession } from 'next-auth';
|
||||||
|
|
||||||
@ -28,29 +28,3 @@ export const getServerSession = async ({ req, res }: GetServerSessionOptions) =>
|
|||||||
|
|
||||||
return { user, session };
|
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 };
|
|
||||||
});
|
|
||||||
|
|||||||
99
packages/lib/server-only/document/resend-document.tsx
Normal file
99
packages/lib/server-only/document/resend-document.tsx
Normal file
@ -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 }),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
};
|
||||||
@ -10,7 +10,7 @@ import slugify from '@sindresorhus/slugify';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import { ONE_HOUR, ONE_SECOND } from '../../constants/time';
|
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';
|
import { alphaid } from '../id';
|
||||||
|
|
||||||
export const getPresignPostUrl = async (fileName: string, contentType: string) => {
|
export const getPresignPostUrl = async (fileName: string, contentType: string) => {
|
||||||
|
|||||||
@ -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 { duplicateDocumentById } from '@documenso/lib/server-only/document/duplicate-document-by-id';
|
||||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-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 { 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 { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||||
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-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';
|
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
||||||
@ -16,6 +17,7 @@ import {
|
|||||||
ZDeleteDraftDocumentMutationSchema,
|
ZDeleteDraftDocumentMutationSchema,
|
||||||
ZGetDocumentByIdQuerySchema,
|
ZGetDocumentByIdQuerySchema,
|
||||||
ZGetDocumentByTokenQuerySchema,
|
ZGetDocumentByTokenQuerySchema,
|
||||||
|
ZResendDocumentMutationSchema,
|
||||||
ZSendDocumentMutationSchema,
|
ZSendDocumentMutationSchema,
|
||||||
ZSetFieldsForDocumentMutationSchema,
|
ZSetFieldsForDocumentMutationSchema,
|
||||||
ZSetRecipientsForDocumentMutationSchema,
|
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
|
duplicateDocument: authenticatedProcedure
|
||||||
.input(ZGetDocumentByIdQuerySchema)
|
.input(ZGetDocumentByIdQuerySchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
|||||||
@ -60,6 +60,11 @@ export const ZSendDocumentMutationSchema = z.object({
|
|||||||
documentId: z.number(),
|
documentId: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ZResendDocumentMutationSchema = z.object({
|
||||||
|
documentId: z.number(),
|
||||||
|
recipients: z.array(z.number()).min(1),
|
||||||
|
});
|
||||||
|
|
||||||
export type TSendDocumentMutationSchema = z.infer<typeof ZSendDocumentMutationSchema>;
|
export type TSendDocumentMutationSchema = z.infer<typeof ZSendDocumentMutationSchema>;
|
||||||
|
|
||||||
export const ZDeleteDraftDocumentMutationSchema = z.object({
|
export const ZDeleteDraftDocumentMutationSchema = z.object({
|
||||||
|
|||||||
@ -9,8 +9,10 @@ import { cn } from '../lib/utils';
|
|||||||
|
|
||||||
const Checkbox = React.forwardRef<
|
const Checkbox = React.forwardRef<
|
||||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> & {
|
||||||
>(({ className, ...props }, ref) => (
|
checkClassName?: string;
|
||||||
|
}
|
||||||
|
>(({ className, checkClassName, ...props }, ref) => (
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -19,8 +21,10 @@ const Checkbox = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator className={cn('text-primary flex items-center justify-center')}>
|
<CheckboxPrimitive.Indicator
|
||||||
<Check className="h-4 w-4" />
|
className={cn('text-primary flex items-center justify-center', checkClassName)}
|
||||||
|
>
|
||||||
|
<Check className="h-3 w-3 stroke-[3px]" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
));
|
));
|
||||||
|
|||||||
@ -11,6 +11,8 @@ const Dialog = DialogPrimitive.Root;
|
|||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger;
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close;
|
||||||
|
|
||||||
const DialogPortal = ({
|
const DialogPortal = ({
|
||||||
children,
|
children,
|
||||||
position = 'start',
|
position = 'start',
|
||||||
@ -51,8 +53,9 @@ const DialogContent = React.forwardRef<
|
|||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||||
position?: 'start' | 'end' | 'center';
|
position?: 'start' | 'end' | 'center';
|
||||||
|
hideClose?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, children, position = 'start', ...props }, ref) => (
|
>(({ className, children, position = 'start', hideClose = false, ...props }, ref) => (
|
||||||
<DialogPortal position={position}>
|
<DialogPortal position={position}>
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
@ -64,10 +67,12 @@ const DialogContent = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
{!hideClose && (
|
||||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
));
|
));
|
||||||
@ -125,4 +130,5 @@ export {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogPortal,
|
DialogPortal,
|
||||||
|
DialogClose,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user