Merge branch 'main' into fix/billing-page

This commit is contained in:
Lucas Smith
2023-12-07 15:37:32 +11:00
committed by GitHub
29 changed files with 529 additions and 301 deletions

View File

@ -29,7 +29,7 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
return ( return (
<div <div
className={cn('relative max-w-[100vw] pt-20 md:pt-28', { className={cn('relative flex min-h-[100vh] max-w-[100vw] flex-col pt-20 md:pt-28', {
'overflow-y-auto overflow-x-hidden': pathname !== '/singleplayer', 'overflow-y-auto overflow-x-hidden': pathname !== '/singleplayer',
})} })}
> >
@ -41,7 +41,7 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" /> <Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
</div> </div>
<div className="relative mx-auto max-w-screen-xl px-4 lg:px-8">{children}</div> <div className="relative mx-auto max-w-screen-xl flex-1 px-4 lg:px-8">{children}</div>
<Footer className="bg-background border-muted mt-24 border-t" /> <Footer className="bg-background border-muted mt-24 border-t" />
</div> </div>

View File

@ -17,15 +17,14 @@ import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add
import type { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types'; import type { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
import { AddSignatureFormPartial } from '@documenso/ui/primitives/document-flow/add-signature'; import { AddSignatureFormPartial } from '@documenso/ui/primitives/document-flow/add-signature';
import type { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types'; import type { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types';
import { import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
DocumentFlowFormContainer,
DocumentFlowFormContainerHeader,
} from '@documenso/ui/primitives/document-flow/document-flow-root';
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; 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 { useToast } from '@documenso/ui/primitives/use-toast';
type SinglePlayerModeStep = 'fields' | 'sign'; const SinglePlayerModeSteps = ['fields', 'sign'] as const;
type SinglePlayerModeStep = (typeof SinglePlayerModeSteps)[number];
// !: This entire file is a hack to get around failed prerendering of // !: This entire file is a hack to get around failed prerendering of
// !: the Single Player Mode page. This regression was introduced during // !: the Single Player Mode page. This regression was introduced during
@ -226,37 +225,35 @@ export const SinglePlayerClient = () => {
</div> </div>
<div className="col-span-12 lg:col-span-6 xl:col-span-5"> <div className="col-span-12 lg:col-span-6 xl:col-span-5">
<DocumentFlowFormContainer className="top-24" onSubmit={(e) => e.preventDefault()}> <DocumentFlowFormContainer
<DocumentFlowFormContainerHeader className="top-24 lg:h-[calc(100vh-7rem)]"
title={currentDocumentFlow.title} onSubmit={(e) => e.preventDefault()}
description={currentDocumentFlow.description} >
/> <Stepper
currentStep={currentDocumentFlow.stepIndex}
{/* Add fields to PDF page. */} setCurrentStep={(step) => setStep(SinglePlayerModeSteps[step - 1])}
{step === 'fields' && ( >
{/* Add fields to PDF page. */}
<fieldset disabled={!uploadedFile} className="flex h-full flex-col"> <fieldset disabled={!uploadedFile} className="flex h-full flex-col">
<AddFieldsFormPartial <AddFieldsFormPartial
documentFlow={documentFlow.fields} documentFlow={documentFlow.fields}
hideRecipients={true} hideRecipients={true}
recipients={uploadedFile ? [placeholderRecipient] : []} recipients={uploadedFile ? [placeholderRecipient] : []}
numberOfSteps={Object.keys(documentFlow).length}
fields={fields} fields={fields}
onSubmit={onFieldsSubmit} onSubmit={onFieldsSubmit}
/> />
</fieldset> </fieldset>
)}
{/* Enter user details and signature. */} {/* Enter user details and signature. */}
{step === 'sign' && (
<AddSignatureFormPartial <AddSignatureFormPartial
documentFlow={documentFlow.sign} documentFlow={documentFlow.sign}
numberOfSteps={Object.keys(documentFlow).length}
fields={fields} fields={fields}
onSubmit={onSignSubmit} onSubmit={onSignSubmit}
requireName={Boolean(fields.find((field) => field.type === 'NAME'))} requireName={Boolean(fields.find((field) => field.type === 'NAME'))}
requireSignature={Boolean(fields.find((field) => field.type === 'SIGNATURE'))} requireSignature={Boolean(fields.find((field) => field.type === 'SIGNATURE'))}
/> />
)} </Stepper>
</DocumentFlowFormContainer> </DocumentFlowFormContainer>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,75 @@
import { expect, test } from '@playwright/test';
import path from 'node:path';
import { TEST_USER } from '@documenso/prisma/seed/pr-718-add-stepper-component';
test(`[PR-718]: should be able to create a document`, async ({ page }) => {
await page.goto('/signin');
const documentTitle = `example-${Date.now()}.pdf`;
// Sign in
await page.getByLabel('Email').fill(TEST_USER.email);
await page.getByLabel('Password', { exact: true }).fill(TEST_USER.password);
await page.getByRole('button', { name: 'Sign In' }).click();
// Upload document
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page.locator('input[type=file]').evaluate((e) => {
if (e instanceof HTMLInputElement) {
e.click();
}
}),
]);
await fileChooser.setFiles(path.join(__dirname, '../../../assets/example.pdf'));
// Wait to be redirected to the edit page
await page.waitForURL(/\/documents\/\d+/);
// Set title
await expect(page.getByRole('heading', { name: 'Add Title' })).toBeVisible();
await page.getByLabel('Title').fill(documentTitle);
await page.getByRole('button', { name: 'Continue' }).click();
// Add signers
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
await page.getByLabel('Email*').fill('user1@example.com');
await page.getByLabel('Name').fill('User 1');
await page.getByRole('button', { name: 'Continue' }).click();
// Add fields
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'User 1 Signature' }).click();
await page.locator('canvas').click({
position: {
x: 100,
y: 100,
},
});
await page.getByRole('button', { name: 'Email Email' }).click();
await page.locator('canvas').click({
position: {
x: 100,
y: 200,
},
});
await page.getByRole('button', { name: 'Continue' }).click();
// Add subject and send
await expect(page.getByRole('heading', { name: 'Add Subject' })).toBeVisible();
await page.getByRole('button', { name: 'Send' }).click();
await page.waitForURL('/documents');
// Assert document was created
await expect(page.getByRole('link', { name: documentTitle })).toBeVisible();
});

View File

@ -0,0 +1,28 @@
import { hashSync } from '@documenso/lib/server-only/auth/hash';
import { prisma } from '..';
//
// https://github.com/documenso/documenso/pull/713
//
const PULL_REQUEST_NUMBER = 718;
const EMAIL_DOMAIN = `pr-${PULL_REQUEST_NUMBER}.documenso.com`;
export const TEST_USER = {
name: 'User 1',
email: `user1@${EMAIL_DOMAIN}`,
password: 'Password123',
} as const;
export const seedDatabase = async () => {
await prisma.user.create({
data: {
name: TEST_USER.name,
email: TEST_USER.email,
password: hashSync(TEST_USER.password),
emailVerified: new Date(),
},
});
};

View File

@ -7,8 +7,9 @@ import { Download } from 'lucide-react';
import { getFile } from '@documenso/lib/universal/upload/get-file'; import { getFile } from '@documenso/lib/universal/upload/get-file';
import type { DocumentData } from '@documenso/prisma/client'; import type { DocumentData } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { Button } from '../../primitives/button';
import { useToast } from '../../primitives/use-toast';
export type DownloadButtonProps = HTMLAttributes<HTMLButtonElement> & { export type DownloadButtonProps = HTMLAttributes<HTMLButtonElement> & {
disabled?: boolean; disabled?: boolean;

View File

@ -13,8 +13,9 @@ import {
} from '@documenso/lib/constants/toast'; } from '@documenso/lib/constants/toast';
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent'; import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { cn } from '../../lib/utils';
import { Button } from '../../primitives/button';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -22,8 +23,8 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '../../primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '../../primitives/use-toast';
export type DocumentShareButtonProps = HTMLAttributes<HTMLButtonElement> & { export type DocumentShareButtonProps = HTMLAttributes<HTMLButtonElement> & {
token?: string; token?: string;

View File

@ -1,17 +1,18 @@
import { TooltipArrow } from '@radix-ui/react-tooltip'; import { TooltipArrow } from '@radix-ui/react-tooltip';
import { VariantProps, cva } from 'class-variance-authority'; import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords'; import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
import { cn } from '@documenso/ui/lib/utils';
import { cn } from '../..//lib/utils';
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@documenso/ui/primitives/tooltip'; } from '../..//primitives/tooltip';
import type { Field } from '.prisma/client';
import { Field } from '.prisma/client';
const tooltipVariants = cva('font-semibold', { const tooltipVariants = cva('font-semibold', {
variants: { variants: {

View File

@ -5,9 +5,10 @@ import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords'; import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
import { Field } from '@documenso/prisma/client'; import type { Field } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { cn } from '../../lib/utils';
import { Card, CardContent } from '../../primitives/card';
export type FieldRootContainerProps = { export type FieldRootContainerProps = {
field: Field; field: Field;

View File

@ -2,14 +2,16 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import Image, { StaticImageData } from 'next/image'; import type { StaticImageData } from 'next/image';
import Image from 'next/image';
import { animate, motion, useMotionTemplate, useMotionValue, useTransform } from 'framer-motion'; import { animate, motion, useMotionTemplate, useMotionValue, useTransform } from 'framer-motion';
import { P, match } from 'ts-pattern'; import { P, match } from 'ts-pattern';
import { Signature } from '@documenso/prisma/client'; import type { Signature } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { cn } from '../lib/utils';
import { Card, CardContent } from '../primitives/card';
export type SigningCardProps = { export type SigningCardProps = {
className?: string; className?: string;

View File

@ -3,16 +3,11 @@ import * as React from 'react';
import { Check, ChevronsUpDown } from 'lucide-react'; import { Check, ChevronsUpDown } from 'lucide-react';
import { Role } from '@documenso/prisma/client'; import { Role } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { cn } from '../lib/utils';
import { import { Button } from './button';
Command, import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from './command';
CommandEmpty, import { Popover, PopoverContent, PopoverTrigger } from './popover';
CommandGroup,
CommandInput,
CommandItem,
} from '@documenso/ui/primitives/command';
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
type ComboboxProps = { type ComboboxProps = {
listValues: string[]; listValues: string[];

View File

@ -1,12 +1,14 @@
'use client'; 'use client';
import { Variants, motion } from 'framer-motion'; import type { Variants } from 'framer-motion';
import { motion } from 'framer-motion';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { cn } from '../lib/utils';
import { Card, CardContent } from './card';
const DocumentDropzoneContainerVariants: Variants = { const DocumentDropzoneContainerVariants: Variants = {
initial: { initial: {

View File

@ -11,29 +11,27 @@ import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-c
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element'; import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { nanoid } from '@documenso/lib/universal/id'; import { nanoid } from '@documenso/lib/universal/id';
import { Field, FieldType, Recipient, SendStatus } from '@documenso/prisma/client'; import type { Field, Recipient } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils'; import { FieldType, SendStatus } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from '@documenso/ui/primitives/command';
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { TAddFieldsFormSchema } from './add-fields.types'; import { cn } from '../../lib/utils';
import { Button } from '../button';
import { Card, CardContent } from '../card';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '../command';
import { Popover, PopoverContent, PopoverTrigger } from '../popover';
import { useStep } from '../stepper';
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
import type { TAddFieldsFormSchema } from './add-fields.types';
import { import {
DocumentFlowFormContainerActions, DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent, DocumentFlowFormContainerContent,
DocumentFlowFormContainerFooter, DocumentFlowFormContainerFooter,
DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from './document-flow-root'; } from './document-flow-root';
import { FieldItem } from './field-item'; import { FieldItem } from './field-item';
import { DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types'; import type { DocumentFlowStep } from './types';
import { FRIENDLY_FIELD_TYPE } from './types';
const fontCaveat = Caveat({ const fontCaveat = Caveat({
weight: ['500'], weight: ['500'],
@ -53,7 +51,6 @@ export type AddFieldsFormProps = {
hideRecipients?: boolean; hideRecipients?: boolean;
recipients: Recipient[]; recipients: Recipient[];
fields: Field[]; fields: Field[];
numberOfSteps: number;
onSubmit: (_data: TAddFieldsFormSchema) => void; onSubmit: (_data: TAddFieldsFormSchema) => void;
}; };
@ -62,10 +59,10 @@ export const AddFieldsFormPartial = ({
hideRecipients = false, hideRecipients = false,
recipients, recipients,
fields, fields,
numberOfSteps,
onSubmit, onSubmit,
}: AddFieldsFormProps) => { }: AddFieldsFormProps) => {
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement(); const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
const { currentStep, totalSteps, previousStep } = useStep();
const { const {
control, control,
@ -287,6 +284,10 @@ export const AddFieldsFormPartial = ({
return ( return (
<> <>
<DocumentFlowFormContainerHeader
title={documentFlow.title}
description={documentFlow.description}
/>
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
<div className="flex flex-col"> <div className="flex flex-col">
{selectedField && ( {selectedField && (
@ -513,15 +514,15 @@ export const AddFieldsFormPartial = ({
<DocumentFlowFormContainerFooter> <DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep <DocumentFlowFormContainerStep
title={documentFlow.title} title={documentFlow.title}
step={documentFlow.stepIndex} step={currentStep}
maxStep={numberOfSteps} maxStep={totalSteps}
/> />
<DocumentFlowFormContainerActions <DocumentFlowFormContainerActions
loading={isSubmitting} loading={isSubmitting}
disabled={isSubmitting} disabled={isSubmitting}
onGoBackClick={() => { onGoBackClick={() => {
documentFlow.onBackStep?.(); previousStep();
remove(); remove();
}} }}
onGoNextClick={() => void onFormSubmit()} onGoNextClick={() => void onFormSubmit()}

View File

@ -9,35 +9,38 @@ 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 { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields'; import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import { Field, FieldType } from '@documenso/prisma/client'; import type { Field } from '@documenso/prisma/client';
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { FieldType } from '@documenso/prisma/client';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { FieldToolTip } from '../../components/field/field-tooltip';
import { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types'; import { cn } from '../../lib/utils';
import { Card, CardContent } from '../card';
import { ElementVisible } from '../element-visible';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
import { Input } from '../input';
import { SignaturePad } from '../signature-pad';
import { useStep } from '../stepper';
import type { TAddSignatureFormSchema } from './add-signature.types';
import { ZAddSignatureFormSchema } from './add-signature.types';
import { import {
DocumentFlowFormContainerActions, DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent, DocumentFlowFormContainerContent,
DocumentFlowFormContainerFooter, DocumentFlowFormContainerFooter,
DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from '@documenso/ui/primitives/document-flow/document-flow-root'; } from './document-flow-root';
import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
import { ZAddSignatureFormSchema } from './add-signature.types';
import { import {
SinglePlayerModeCustomTextField, SinglePlayerModeCustomTextField,
SinglePlayerModeSignatureField, SinglePlayerModeSignatureField,
} from './single-player-mode-fields'; } from './single-player-mode-fields';
import type { DocumentFlowStep } from './types';
export type AddSignatureFormProps = { export type AddSignatureFormProps = {
defaultValues?: TAddSignatureFormSchema; defaultValues?: TAddSignatureFormSchema;
documentFlow: DocumentFlowStep; documentFlow: DocumentFlowStep;
fields: FieldWithSignature[]; fields: FieldWithSignature[];
numberOfSteps: number;
onSubmit: (_data: TAddSignatureFormSchema) => Promise<void> | void; onSubmit: (_data: TAddSignatureFormSchema) => Promise<void> | void;
requireName?: boolean; requireName?: boolean;
requireSignature?: boolean; requireSignature?: boolean;
@ -47,11 +50,13 @@ export const AddSignatureFormPartial = ({
defaultValues, defaultValues,
documentFlow, documentFlow,
fields, fields,
numberOfSteps,
onSubmit, onSubmit,
requireName = false, requireName = false,
requireSignature = true, requireSignature = true,
}: AddSignatureFormProps) => { }: AddSignatureFormProps) => {
const { currentStep, totalSteps } = useStep();
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false); const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
// Refined schema which takes into account whether to allow an empty name or signature. // Refined schema which takes into account whether to allow an empty name or signature.
@ -206,46 +211,30 @@ export const AddSignatureFormPartial = ({
}; };
return ( return (
<Form {...form}> <>
<fieldset className="flex h-full flex-col" disabled={form.formState.isSubmitting}> <DocumentFlowFormContainerHeader
<DocumentFlowFormContainerContent> title={documentFlow.title}
<div className="space-y-4"> description={documentFlow.description}
<FormField />
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>Email</FormLabel>
<FormControl>
<Input
className="bg-background"
type="email"
autoComplete="email"
{...field}
onChange={(value) => {
onFormValueChange(FieldType.EMAIL);
field.onChange(value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{requireName && ( <Form {...form}>
<fieldset className="flex h-full flex-col" disabled={form.formState.isSubmitting}>
<DocumentFlowFormContainerContent>
<div className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="name" name="email"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel required={requireName}>Name</FormLabel> <FormLabel required>Email</FormLabel>
<FormControl> <FormControl>
<Input <Input
className="bg-background" className="bg-background"
type="email"
autoComplete="email"
{...field} {...field}
onChange={(value) => { onChange={(value) => {
onFormValueChange(FieldType.NAME); onFormValueChange(FieldType.EMAIL);
field.onChange(value); field.onChange(value);
}} }}
/> />
@ -254,91 +243,114 @@ export const AddSignatureFormPartial = ({
</FormItem> </FormItem>
)} )}
/> />
)}
{requireSignature && ( {requireName && (
<FormField <FormField
control={form.control} control={form.control}
name="signature" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel required={requireSignature}>Signature</FormLabel> <FormLabel required={requireName}>Name</FormLabel>
<FormControl> <FormControl>
<Card <Input
className={cn('mt-2', { className="bg-background"
'rounded-sm ring-2 ring-red-500 ring-offset-2 transition-all': {...field}
form.formState.errors.signature, onChange={(value) => {
})} onFormValueChange(FieldType.NAME);
gradient={!form.formState.errors.signature} field.onChange(value);
degrees={-120} }}
> />
<CardContent className="p-0"> </FormControl>
<SignaturePad <FormMessage />
className="h-44 w-full" </FormItem>
defaultValue={field.value} )}
onBlur={field.onBlur} />
onChange={(value) => { )}
onFormValueChange(FieldType.SIGNATURE);
field.onChange(value);
}}
/>
</CardContent>
</Card>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
</DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter> {requireSignature && (
<DocumentFlowFormContainerStep <FormField
title={documentFlow.title} control={form.control}
step={documentFlow.stepIndex} name="signature"
maxStep={numberOfSteps} render={({ field }) => (
/> <FormItem>
<FormLabel required={requireSignature}>Signature</FormLabel>
<FormControl>
<Card
className={cn('mt-2', {
'rounded-sm ring-2 ring-red-500 ring-offset-2 transition-all':
form.formState.errors.signature,
})}
gradient={!form.formState.errors.signature}
degrees={-120}
>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
defaultValue={field.value}
onBlur={field.onBlur}
onChange={(value) => {
onFormValueChange(FieldType.SIGNATURE);
field.onChange(value);
}}
/>
</CardContent>
</Card>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
</DocumentFlowFormContainerContent>
<DocumentFlowFormContainerActions <DocumentFlowFormContainerFooter>
loading={form.formState.isSubmitting} <DocumentFlowFormContainerStep
disabled={form.formState.isSubmitting} title={documentFlow.title}
onGoBackClick={documentFlow.onBackStep} step={currentStep}
onGoNextClick={form.handleSubmit(onValidateFields)} maxStep={totalSteps}
/> />
</DocumentFlowFormContainerFooter>
</fieldset>
{validateUninsertedFields && uninsertedFields[0] && ( <DocumentFlowFormContainerActions
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning"> loading={form.formState.isSubmitting}
Click to insert field disabled={form.formState.isSubmitting}
</FieldToolTip> onGoBackClick={documentFlow.onBackStep}
)} onGoNextClick={form.handleSubmit(onValidateFields)}
/>
</DocumentFlowFormContainerFooter>
</fieldset>
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}> {validateUninsertedFields && uninsertedFields[0] && (
{localFields.map((field) => <FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
match(field.type) Click to insert field
.with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, () => { </FieldToolTip>
return ( )}
<SinglePlayerModeCustomTextField
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{localFields.map((field) =>
match(field.type)
.with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, () => {
return (
<SinglePlayerModeCustomTextField
onClick={insertField(field)}
key={field.id}
field={field}
/>
);
})
.with(FieldType.SIGNATURE, () => (
<SinglePlayerModeSignatureField
onClick={insertField(field)} onClick={insertField(field)}
key={field.id} key={field.id}
field={field} field={field}
/> />
); ))
}) .otherwise(() => {
.with(FieldType.SIGNATURE, () => ( return null;
<SinglePlayerModeSignatureField }),
onClick={insertField(field)} )}
key={field.id} </ElementVisible>
field={field} </Form>
/> </>
))
.otherwise(() => {
return null;
}),
)}
</ElementVisible>
</Form>
); );
}; };

View File

@ -9,35 +9,37 @@ import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { nanoid } from '@documenso/lib/universal/id'; import { nanoid } from '@documenso/lib/universal/id';
import { DocumentStatus, Field, Recipient, SendStatus } from '@documenso/prisma/client'; import type { Field, Recipient } from '@documenso/prisma/client';
import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button'; import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { TAddSignersFormSchema, ZAddSignersFormSchema } from './add-signers.types'; import { Button } from '../button';
import { FormErrorMessage } from '../form/form-error-message';
import { Input } from '../input';
import { Label } from '../label';
import { useStep } from '../stepper';
import { useToast } from '../use-toast';
import type { TAddSignersFormSchema } from './add-signers.types';
import { ZAddSignersFormSchema } from './add-signers.types';
import { import {
DocumentFlowFormContainerActions, DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent, DocumentFlowFormContainerContent,
DocumentFlowFormContainerFooter, DocumentFlowFormContainerFooter,
DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from './document-flow-root'; } from './document-flow-root';
import { DocumentFlowStep } from './types'; import type { DocumentFlowStep } from './types';
export type AddSignersFormProps = { export type AddSignersFormProps = {
documentFlow: DocumentFlowStep; documentFlow: DocumentFlowStep;
recipients: Recipient[]; recipients: Recipient[];
fields: Field[]; fields: Field[];
document: DocumentWithData; document: DocumentWithData;
numberOfSteps: number;
onSubmit: (_data: TAddSignersFormSchema) => void; onSubmit: (_data: TAddSignersFormSchema) => void;
}; };
export const AddSignersFormPartial = ({ export const AddSignersFormPartial = ({
documentFlow, documentFlow,
numberOfSteps,
recipients, recipients,
document, document,
fields: _fields, fields: _fields,
@ -48,6 +50,8 @@ export const AddSignersFormPartial = ({
const initialId = useId(); const initialId = useId();
const { currentStep, totalSteps, previousStep } = useStep();
const { const {
control, control,
handleSubmit, handleSubmit,
@ -126,6 +130,10 @@ export const AddSignersFormPartial = ({
return ( return (
<> <>
<DocumentFlowFormContainerHeader
title={documentFlow.title}
description={documentFlow.description}
/>
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
<div className="flex w-full flex-col gap-y-4"> <div className="flex w-full flex-col gap-y-4">
<AnimatePresence> <AnimatePresence>
@ -221,15 +229,15 @@ export const AddSignersFormPartial = ({
<DocumentFlowFormContainerFooter> <DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep <DocumentFlowFormContainerStep
title={documentFlow.title} title={documentFlow.title}
step={documentFlow.stepIndex} step={currentStep}
maxStep={numberOfSteps} maxStep={totalSteps}
/> />
<DocumentFlowFormContainerActions <DocumentFlowFormContainerActions
canGoBack={document.status === DocumentStatus.DRAFT} canGoBack={document.status === DocumentStatus.DRAFT}
loading={isSubmitting} loading={isSubmitting}
disabled={isSubmitting} disabled={isSubmitting}
onGoBackClick={documentFlow.onBackStep} onGoBackClick={previousStep}
onGoNextClick={() => void onFormSubmit()} onGoNextClick={() => void onFormSubmit()}
/> />
</DocumentFlowFormContainerFooter> </DocumentFlowFormContainerFooter>

View File

@ -2,28 +2,30 @@
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { DocumentStatus, Field, Recipient } from '@documenso/prisma/client'; import type { Field, Recipient } from '@documenso/prisma/client';
import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import { DocumentStatus } from '@documenso/prisma/client';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { Textarea } from '@documenso/ui/primitives/textarea';
import { TAddSubjectFormSchema } from './add-subject.types'; import { FormErrorMessage } from '../form/form-error-message';
import { Input } from '../input';
import { Label } from '../label';
import { useStep } from '../stepper';
import { Textarea } from '../textarea';
import type { TAddSubjectFormSchema } from './add-subject.types';
import { import {
DocumentFlowFormContainerActions, DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent, DocumentFlowFormContainerContent,
DocumentFlowFormContainerFooter, DocumentFlowFormContainerFooter,
DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from './document-flow-root'; } from './document-flow-root';
import { DocumentFlowStep } from './types'; import type { DocumentFlowStep } from './types';
export type AddSubjectFormProps = { export type AddSubjectFormProps = {
documentFlow: DocumentFlowStep; documentFlow: DocumentFlowStep;
recipients: Recipient[]; recipients: Recipient[];
fields: Field[]; fields: Field[];
document: DocumentWithData; document: DocumentWithData;
numberOfSteps: number;
onSubmit: (_data: TAddSubjectFormSchema) => void; onSubmit: (_data: TAddSubjectFormSchema) => void;
}; };
@ -32,7 +34,6 @@ export const AddSubjectFormPartial = ({
recipients: _recipients, recipients: _recipients,
fields: _fields, fields: _fields,
document, document,
numberOfSteps,
onSubmit, onSubmit,
}: AddSubjectFormProps) => { }: AddSubjectFormProps) => {
const { const {
@ -49,9 +50,14 @@ export const AddSubjectFormPartial = ({
}); });
const onFormSubmit = handleSubmit(onSubmit); const onFormSubmit = handleSubmit(onSubmit);
const { currentStep, totalSteps, previousStep } = useStep();
return ( return (
<> <>
<DocumentFlowFormContainerHeader
title={documentFlow.title}
description={documentFlow.description}
/>
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
@ -124,15 +130,15 @@ export const AddSubjectFormPartial = ({
<DocumentFlowFormContainerFooter> <DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep <DocumentFlowFormContainerStep
title={documentFlow.title} title={documentFlow.title}
step={documentFlow.stepIndex} step={currentStep}
maxStep={numberOfSteps} maxStep={totalSteps}
/> />
<DocumentFlowFormContainerActions <DocumentFlowFormContainerActions
loading={isSubmitting} loading={isSubmitting}
disabled={isSubmitting} disabled={isSubmitting}
goNextLabel={document.status === DocumentStatus.DRAFT ? 'Send' : 'Update'} goNextLabel={document.status === DocumentStatus.DRAFT ? 'Send' : 'Update'}
onGoBackClick={documentFlow.onBackStep} onGoBackClick={previousStep}
onGoNextClick={() => void onFormSubmit()} onGoNextClick={() => void onFormSubmit()}
/> />
</DocumentFlowFormContainerFooter> </DocumentFlowFormContainerFooter>

View File

@ -4,15 +4,17 @@ import { useForm } from 'react-hook-form';
import type { Field, Recipient } from '@documenso/prisma/client'; import type { Field, Recipient } from '@documenso/prisma/client';
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { FormErrorMessage } from '../form/form-error-message';
import { Input } from '../input';
import { Label } from '../label';
import { useStep } from '../stepper';
import type { TAddTitleFormSchema } from './add-title.types'; import type { TAddTitleFormSchema } from './add-title.types';
import { import {
DocumentFlowFormContainerActions, DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent, DocumentFlowFormContainerContent,
DocumentFlowFormContainerFooter, DocumentFlowFormContainerFooter,
DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from './document-flow-root'; } from './document-flow-root';
import type { DocumentFlowStep } from './types'; import type { DocumentFlowStep } from './types';
@ -22,7 +24,6 @@ export type AddTitleFormProps = {
recipients: Recipient[]; recipients: Recipient[];
fields: Field[]; fields: Field[];
document: DocumentWithData; document: DocumentWithData;
numberOfSteps: number;
onSubmit: (_data: TAddTitleFormSchema) => void; onSubmit: (_data: TAddTitleFormSchema) => void;
}; };
@ -31,7 +32,6 @@ export const AddTitleFormPartial = ({
recipients: _recipients, recipients: _recipients,
fields: _fields, fields: _fields,
document, document,
numberOfSteps,
onSubmit, onSubmit,
}: AddTitleFormProps) => { }: AddTitleFormProps) => {
const { const {
@ -46,8 +46,14 @@ export const AddTitleFormPartial = ({
const onFormSubmit = handleSubmit(onSubmit); const onFormSubmit = handleSubmit(onSubmit);
const { stepIndex, currentStep, totalSteps, previousStep } = useStep();
return ( return (
<> <>
<DocumentFlowFormContainerHeader
title={documentFlow.title}
description={documentFlow.description}
/>
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
@ -72,14 +78,15 @@ export const AddTitleFormPartial = ({
<DocumentFlowFormContainerFooter> <DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep <DocumentFlowFormContainerStep
title={documentFlow.title} title={documentFlow.title}
step={documentFlow.stepIndex} step={currentStep}
maxStep={numberOfSteps} maxStep={totalSteps}
/> />
<DocumentFlowFormContainerActions <DocumentFlowFormContainerActions
loading={isSubmitting} loading={isSubmitting}
disabled={isSubmitting} disabled={isSubmitting}
onGoBackClick={documentFlow.onBackStep} canGoBack={stepIndex !== 0}
onGoBackClick={previousStep}
onGoNextClick={() => void onFormSubmit()} onGoNextClick={() => void onFormSubmit()}
/> />
</DocumentFlowFormContainerFooter> </DocumentFlowFormContainerFooter>

View File

@ -1,11 +1,12 @@
'use client'; 'use client';
import React, { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import React from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '../../lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '../button';
export type DocumentFlowFormContainerProps = HTMLAttributes<HTMLFormElement> & { export type DocumentFlowFormContainerProps = HTMLAttributes<HTMLFormElement> & {
children?: React.ReactNode; children?: React.ReactNode;

View File

@ -7,10 +7,11 @@ import { createPortal } from 'react-dom';
import { Rnd } from 'react-rnd'; import { Rnd } from 'react-rnd';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { FRIENDLY_FIELD_TYPE, TDocumentFlowFormSchema } from './types'; import { cn } from '../../lib/utils';
import { Card, CardContent } from '../card';
import type { TDocumentFlowFormSchema } from './types';
import { FRIENDLY_FIELD_TYPE } from './types';
type Field = TDocumentFlowFormSchema['fields'][0]; type Field = TDocumentFlowFormSchema['fields'][0];

View File

@ -2,7 +2,8 @@ import { useState } from 'react';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { Button, ButtonProps } from '@documenso/ui/primitives/button'; import type { ButtonProps } from '../button';
import { Button } from '../button';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -11,7 +12,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '../dialog';
export type SendDocumentActionDialogProps = ButtonProps & { export type SendDocumentActionDialogProps = ButtonProps & {
loading?: boolean; loading?: boolean;

View File

@ -13,9 +13,11 @@ import {
MIN_HANDWRITING_FONT_SIZE, MIN_HANDWRITING_FONT_SIZE,
MIN_STANDARD_FONT_SIZE, MIN_STANDARD_FONT_SIZE,
} from '@documenso/lib/constants/pdf'; } from '@documenso/lib/constants/pdf';
import { Field, FieldType } from '@documenso/prisma/client'; import type { Field } from '@documenso/prisma/client';
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { FieldType } from '@documenso/prisma/client';
import { FieldRootContainer } from '@documenso/ui/components/field/field'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { FieldRootContainer } from '../../components/field/field';
export type SinglePlayerModeFieldContainerProps = { export type SinglePlayerModeFieldContainerProps = {
field: FieldWithSignature; field: FieldWithSignature;

View File

@ -53,7 +53,7 @@ export const FRIENDLY_FIELD_TYPE: Record<FieldType, string> = {
export interface DocumentFlowStep { export interface DocumentFlowStep {
title: string; title: string;
description: string; description: string;
stepIndex: number; stepIndex?: number;
onBackStep?: () => unknown; onBackStep?: () => unknown;
onNextStep?: () => unknown; onNextStep?: () => unknown;
} }

View File

@ -1,6 +1,6 @@
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '../../lib/utils';
export type FormErrorMessageProps = { export type FormErrorMessageProps = {
className?: string; className?: string;

View File

@ -1,19 +1,12 @@
import * as React from 'react'; import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label'; import type * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot'; import { Slot } from '@radix-ui/react-slot';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
Controller, import { Controller, FormProvider, useFormContext } from 'react-hook-form';
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from 'react-hook-form';
import { cn } from '@documenso/ui/lib/utils';
import { cn } from '../../lib/utils';
import { Label } from '../label'; import { Label } from '../label';
const Form = FormProvider; const Form = FormProvider;

View File

@ -3,16 +3,16 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { PDFDocumentProxy } from 'pdfjs-dist'; import type { PDFDocumentProxy } from 'pdfjs-dist';
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf'; import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css'; import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css'; import 'react-pdf/dist/esm/Page/TextLayer.css';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getFile } from '@documenso/lib/universal/upload/get-file'; import { getFile } from '@documenso/lib/universal/upload/get-file';
import { DocumentData } from '@documenso/prisma/client'; import type { DocumentData } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { cn } from '../lib/utils';
import { useToast } from './use-toast'; import { useToast } from './use-toast';
export type LoadedPDFDocument = PDFDocumentProxy; export type LoadedPDFDocument = PDFDocumentProxy;

View File

@ -1,20 +1,12 @@
'use client'; 'use client';
import { import type { HTMLAttributes, MouseEvent, PointerEvent, TouchEvent } from 'react';
HTMLAttributes, import { useEffect, useMemo, useRef, useState } from 'react';
MouseEvent,
PointerEvent,
TouchEvent,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { StrokeOptions, getStroke } from 'perfect-freehand'; import type { StrokeOptions } from 'perfect-freehand';
import { getStroke } from 'perfect-freehand';
import { cn } from '@documenso/ui/lib/utils';
import { cn } from '../../lib/utils';
import { getSvgPathFromStroke } from './helper'; import { getSvgPathFromStroke } from './helper';
import { Point } from './point'; import { Point } from './point';

View File

@ -0,0 +1,109 @@
import React, { createContext, useContext, useState } from 'react';
import type { FC } from 'react';
type StepContextValue = {
isCompleting: boolean;
stepIndex: number;
currentStep: number;
totalSteps: number;
isFirst: boolean;
isLast: boolean;
nextStep: () => void;
previousStep: () => void;
};
const StepContext = createContext<StepContextValue | null>(null);
type StepperProps = {
children: React.ReactNode;
onComplete?: () => void | Promise<void>;
onStepChanged?: (currentStep: number) => void;
currentStep?: number; // external control prop
setCurrentStep?: (step: number) => void; // external control function
};
export const Stepper: FC<StepperProps> = ({
children,
onComplete,
onStepChanged,
currentStep: propCurrentStep,
setCurrentStep: propSetCurrentStep,
}) => {
const [stateCurrentStep, stateSetCurrentStep] = useState(1);
const [isCompleting, setIsCompleting] = useState(false);
// Determine if props are provided, otherwise use state
const isControlled = propCurrentStep !== undefined && propSetCurrentStep !== undefined;
const currentStep = isControlled ? propCurrentStep : stateCurrentStep;
const setCurrentStep = isControlled ? propSetCurrentStep : stateSetCurrentStep;
const totalSteps = React.Children.count(children);
const handleComplete = async () => {
try {
if (!onComplete) {
return;
}
setIsCompleting(true);
await onComplete();
setIsCompleting(false);
} catch (error) {
setIsCompleting(false);
throw error;
}
};
const handleStepChange = (nextStep: number) => {
setCurrentStep(nextStep);
onStepChanged?.(nextStep);
};
const nextStep = () => {
if (currentStep < totalSteps) {
void handleStepChange(currentStep + 1);
} else {
void handleComplete();
}
};
const previousStep = () => {
if (currentStep > 1) {
void handleStepChange(currentStep - 1);
}
};
// Empty stepper
if (totalSteps === 0) {
return null;
}
const currentChild = React.Children.toArray(children)[currentStep - 1];
const stepContextValue: StepContextValue = {
isCompleting,
stepIndex: currentStep - 1,
currentStep,
totalSteps,
isFirst: currentStep === 1,
isLast: currentStep === totalSteps,
nextStep,
previousStep,
};
return <StepContext.Provider value={stepContextValue}>{currentChild}</StepContext.Provider>;
};
/** Hook for children to use the step context */
export const useStep = (): StepContextValue => {
const context = useContext(StepContext);
if (!context) {
throw new Error('useStep must be used within a Stepper');
}
return context;
};