Merge branch 'main' into feat/public-api

This commit is contained in:
Catalin Pit
2023-12-11 14:44:29 +02:00
committed by GitHub
61 changed files with 1004 additions and 411 deletions

View File

@ -4,8 +4,8 @@ import { useState } from 'react';
import { useRouter } from 'next/navigation';
import type { DocumentData, Field, Recipient, User } from '@documenso/prisma/client';
import { DocumentStatus } from '@documenso/prisma/client';
import type { DocumentData, Field, Recipient, User } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
@ -18,12 +18,10 @@ import { AddSubjectFormPartial } from '@documenso/ui/primitives/document-flow/ad
import type { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
import { AddTitleFormPartial } from '@documenso/ui/primitives/document-flow/add-title';
import type { TAddTitleFormSchema } from '@documenso/ui/primitives/document-flow/add-title.types';
import {
DocumentFlowFormContainer,
DocumentFlowFormContainerHeader,
} from '@documenso/ui/primitives/document-flow/document-flow-root';
import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { Stepper } from '@documenso/ui/primitives/stepper';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type EditDocumentFormProps = {
@ -36,6 +34,7 @@ export type EditDocumentFormProps = {
};
type EditDocumentStep = 'title' | 'signers' | 'fields' | 'subject';
const EditDocumentSteps: EditDocumentStep[] = ['title', 'signers', 'fields', 'subject'];
export const EditDocumentForm = ({
className,
@ -48,6 +47,7 @@ export const EditDocumentForm = ({
const { toast } = useToast();
const router = useRouter();
// controlled stepper state
const [step, setStep] = useState<EditDocumentStep>(
document.status === DocumentStatus.DRAFT ? 'title' : 'signers',
);
@ -67,24 +67,19 @@ export const EditDocumentForm = ({
title: 'Add Signers',
description: 'Add the people who will sign the document.',
stepIndex: 2,
onBackStep: () => document.status === DocumentStatus.DRAFT && setStep('title'),
},
fields: {
title: 'Add Fields',
description: 'Add all relevant fields for each recipient.',
stepIndex: 3,
onBackStep: () => setStep('signers'),
},
subject: {
title: 'Add Subject',
description: 'Add the subject and message you wish to send to signers.',
stepIndex: 4,
onBackStep: () => setStep('fields'),
},
};
const currentDocumentFlow = documentFlow[step];
const onAddTitleFormSubmit = async (data: TAddTitleFormSchema) => {
try {
// Custom invocation server action
@ -116,7 +111,6 @@ export const EditDocumentForm = ({
});
router.refresh();
setStep('fields');
} catch (err) {
console.error(err);
@ -138,7 +132,6 @@ export const EditDocumentForm = ({
});
router.refresh();
setStep('subject');
} catch (err) {
console.error(err);
@ -181,6 +174,8 @@ export const EditDocumentForm = ({
}
};
const currentDocumentFlow = documentFlow[step];
return (
<div className={cn('grid w-full grid-cols-12 gap-8', className)}>
<Card
@ -197,56 +192,43 @@ export const EditDocumentForm = ({
className="lg:h-[calc(100vh-6rem)]"
onSubmit={(e) => e.preventDefault()}
>
<DocumentFlowFormContainerHeader
title={currentDocumentFlow.title}
description={currentDocumentFlow.description}
/>
{step === 'title' && (
<Stepper
currentStep={currentDocumentFlow.stepIndex}
setCurrentStep={(step) => setStep(EditDocumentSteps[step - 1])}
>
<AddTitleFormPartial
key={recipients.length}
documentFlow={documentFlow.title}
recipients={recipients}
fields={fields}
document={document}
numberOfSteps={Object.keys(documentFlow).length}
onSubmit={onAddTitleFormSubmit}
/>
)}
{step === 'signers' && (
<AddSignersFormPartial
key={recipients.length}
documentFlow={documentFlow.signers}
document={document}
recipients={recipients}
fields={fields}
numberOfSteps={Object.keys(documentFlow).length}
onSubmit={onAddSignersFormSubmit}
/>
)}
{step === 'fields' && (
<AddFieldsFormPartial
key={fields.length}
documentFlow={documentFlow.fields}
recipients={recipients}
fields={fields}
numberOfSteps={Object.keys(documentFlow).length}
onSubmit={onAddFieldsFormSubmit}
/>
)}
{step === 'subject' && (
<AddSubjectFormPartial
key={recipients.length}
documentFlow={documentFlow.subject}
document={document}
recipients={recipients}
fields={fields}
numberOfSteps={Object.keys(documentFlow).length}
onSubmit={onAddSubjectFormSubmit}
/>
)}
</Stepper>
</DocumentFlowFormContainer>
</div>
</div>

View File

@ -12,6 +12,7 @@ import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DataTableActionButtonProps = {
row: Document & {
@ -22,6 +23,7 @@ export type DataTableActionButtonProps = {
export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
const { data: session } = useSession();
const { toast } = useToast();
if (!session) {
return null;
@ -37,39 +39,47 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
const onDownloadClick = async () => {
let document: DocumentWithData | null = null;
try {
let document: DocumentWithData | null = null;
if (!recipient) {
document = await trpcClient.document.getDocumentById.query({
id: row.id,
if (!recipient) {
document = await trpcClient.document.getDocumentById.query({
id: row.id,
});
} else {
document = await trpcClient.document.getDocumentByToken.query({
token: recipient.token,
});
}
const documentData = document?.documentData;
if (!documentData) {
return;
}
const documentBytes = await getFile(documentData);
const blob = new Blob([documentBytes], {
type: 'application/pdf',
});
} else {
document = await trpcClient.document.getDocumentByToken.query({
token: recipient.token,
const link = window.document.createElement('a');
const baseTitle = row.title.includes('.pdf') ? row.title.split('.pdf')[0] : row.title;
link.href = window.URL.createObjectURL(blob);
link.download = baseTitle ? `${baseTitle}_signed.pdf` : 'document.pdf';
link.click();
window.URL.revokeObjectURL(link.href);
} catch (error) {
toast({
title: 'Something went wrong',
description: 'An error occurred while trying to download file.',
variant: 'destructive',
});
}
const documentData = document?.documentData;
if (!documentData) {
return;
}
const documentBytes = await getFile(documentData);
const blob = new Blob([documentBytes], {
type: 'application/pdf',
});
const link = window.document.createElement('a');
const baseTitle = row.title.includes('.pdf') ? row.title.split('.pdf')[0] : row.title;
link.href = window.URL.createObjectURL(blob);
link.download = baseTitle ? `${baseTitle}_signed.pdf` : 'document.pdf';
link.click();
window.URL.revokeObjectURL(link.href);
};
return match({

View File

@ -9,6 +9,7 @@ import { Loader } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { putFile } from '@documenso/lib/universal/upload/put-file';
import { TRPCClientError } from '@documenso/trpc/client';
@ -23,6 +24,7 @@ export type UploadDocumentProps = {
export const UploadDocument = ({ className }: UploadDocumentProps) => {
const router = useRouter();
const analytics = useAnalytics();
const { data: session } = useSession();
const { toast } = useToast();
@ -55,6 +57,12 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
duration: 5000,
});
analytics.capture('App: Document Uploaded', {
userId: session?.user.id,
documentId: id,
timestamp: new Date().toISOString(),
});
router.push(`/documents/${id}`);
} catch (error) {
console.error(error);

View File

@ -23,7 +23,7 @@ export const createCheckout = async ({ priceId }: CreateCheckoutOptions) => {
let stripeCustomer: Stripe.Customer | null = null;
// Find the Stripe customer for the current user subscription.
if (existingSubscription) {
if (existingSubscription?.periodEnd && existingSubscription.periodEnd >= new Date()) {
stripeCustomer = await getStripeCustomerById(existingSubscription.customerId);
if (!stripeCustomer) {

View File

@ -7,6 +7,7 @@ import { useRouter } from 'next/navigation';
import { useSession } from 'next-auth/react';
import { useForm } from 'react-hook-form';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { Document, Field, Recipient } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
@ -29,6 +30,7 @@ export type SigningFormProps = {
export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => {
const router = useRouter();
const analytics = useAnalytics();
const { data: session } = useSession();
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
@ -60,6 +62,12 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
documentId: document.id,
});
analytics.capture('App: Recipient has completed signing', {
signerId: recipient.id,
documentId: document.id,
timestamp: new Date().toISOString(),
});
router.push(`/sign/${recipient.token}/complete`);
};

View File

@ -79,8 +79,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
return searchDocumentsData.map((document) => ({
label: document.title,
path: `/documents/${document.id}`,
value:
document.title + ' ' + document.Recipient.map((recipient) => recipient.email).join(' '),
value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '),
}));
}, [searchDocumentsData]);

View File

@ -1,10 +1,11 @@
'use client';
import { HTMLAttributes, useEffect, useState } from 'react';
import type { HTMLAttributes } from 'react';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { User } from '@documenso/prisma/client';
import type { User } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Logo } from '~/components/branding/logo';
@ -32,7 +33,7 @@ export const Header = ({ className, user, ...props }: HeaderProps) => {
return (
<header
className={cn(
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-50 flex h-16 w-full items-center border-b border-b-transparent backdrop-blur duration-200',
'supports-backdrop-blur:bg-background/60 bg-background/95 sticky top-0 z-[1000] flex h-16 w-full items-center border-b border-b-transparent backdrop-blur duration-200',
scrollY > 5 && 'border-b-border',
className,
)}

View File

@ -8,6 +8,7 @@ import { signIn } from 'next-auth/react';
import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
@ -36,6 +37,7 @@ export type SignUpFormProps = {
export const SignUpForm = ({ className }: SignUpFormProps) => {
const { toast } = useToast();
const analytics = useAnalytics();
const [showPassword, setShowPassword] = useState(false);
const {
@ -64,6 +66,11 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
password,
callbackUrl: '/',
});
analytics.capture('App: User Sign Up', {
email,
timestamp: new Date().toISOString(),
});
} catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
toast({

View File

@ -4,6 +4,11 @@ import { appRouter } from '@documenso/trpc/server/router';
export const config = {
maxDuration: 60,
api: {
bodyParser: {
sizeLimit: '50mb',
},
},
};
export default trpcNext.createNextApiHandler({