mirror of
https://github.com/documenso/documenso.git
synced 2025-11-09 20:12:31 +10:00
feat: update document flow
- Fixed z-index when dragging pre-existing fields - Refactored document flow - Added button spinner - Added animation for document flow slider - Updated drag and drop fields - Updated document flow so it adjusts to the height of the PDF - Updated claim plan dialog
This commit is contained in:
@ -5,7 +5,7 @@ import React, { useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Info, Loader } from 'lucide-react';
|
||||
import { Info } from 'lucide-react';
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
@ -85,7 +85,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Dialog open={open} onOpenChange={(value) => !isSubmitting && setOpen(value)}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
@ -97,50 +97,49 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form
|
||||
className={cn('flex flex-col gap-y-4', className)}
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
>
|
||||
{params?.get('cancelled') === 'true' && (
|
||||
<div className="rounded-lg border border-yellow-400 bg-yellow-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<Info className="h-5 w-5 text-yellow-400" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm leading-5 text-yellow-700">
|
||||
You have cancelled the payment process. If you didn't mean to do this, please
|
||||
try again.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<fieldset disabled={isSubmitting} className={cn('flex flex-col gap-y-4', className)}>
|
||||
{params?.get('cancelled') === 'true' && (
|
||||
<div className="rounded-lg border border-yellow-400 bg-yellow-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<Info className="h-5 w-5 text-yellow-400" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm leading-5 text-yellow-700">
|
||||
You have cancelled the payment process. If you didn't mean to do this, please
|
||||
try again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label className="text-slate-500">Name</Label>
|
||||
|
||||
<Input type="text" className="mt-2" {...register('name')} autoFocus />
|
||||
|
||||
<FormErrorMessage className="mt-1" error={errors.name} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label className="text-slate-500">Name</Label>
|
||||
<div>
|
||||
<Label className="text-slate-500">Email</Label>
|
||||
|
||||
<Input type="text" className="mt-2" {...register('name')} autoFocus />
|
||||
<Input type="email" className="mt-2" {...register('email')} />
|
||||
|
||||
<FormErrorMessage className="mt-1" error={errors.name} />
|
||||
</div>
|
||||
<FormErrorMessage className="mt-1" error={errors.email} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-slate-500">Email</Label>
|
||||
|
||||
<Input type="email" className="mt-2" {...register('email')} />
|
||||
|
||||
<FormErrorMessage className="mt-1" error={errors.email} />
|
||||
</div>
|
||||
|
||||
<Button type="submit" size="lg" disabled={isSubmitting}>
|
||||
{isSubmitting && <Loader className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Claim the Community Plan ({/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
|
||||
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
||||
? 'Monthly'
|
||||
: 'Yearly'}
|
||||
)
|
||||
</Button>
|
||||
<Button type="submit" size="lg" loading={isSubmitting}>
|
||||
Claim the Community Plan (
|
||||
{/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
|
||||
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
||||
? 'Monthly'
|
||||
: 'Yearly'}
|
||||
)
|
||||
</Button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@ -13,6 +13,11 @@ import { AddSignersFormPartial } from '@documenso/ui/primitives/document-flow/ad
|
||||
import { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types';
|
||||
import { AddSubjectFormPartial } from '@documenso/ui/primitives/document-flow/add-subject';
|
||||
import { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
|
||||
import {
|
||||
DocumentFlowFormContainer,
|
||||
DocumentFlowFormContainerHeader,
|
||||
} from '@documenso/ui/primitives/document-flow/document-flow-root';
|
||||
import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
|
||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
@ -28,6 +33,8 @@ export type EditDocumentFormProps = {
|
||||
fields: Field[];
|
||||
};
|
||||
|
||||
type EditDocumentStep = 'signers' | 'fields' | 'subject';
|
||||
|
||||
export const EditDocumentForm = ({
|
||||
className,
|
||||
document,
|
||||
@ -38,29 +45,34 @@ export const EditDocumentForm = ({
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const [step, setStep] = useState<'signers' | 'fields' | 'subject'>('signers');
|
||||
const [step, setStep] = useState<EditDocumentStep>('signers');
|
||||
|
||||
const documentUrl = `data:application/pdf;base64,${document.document}`;
|
||||
|
||||
const onNextStep = () => {
|
||||
if (step === 'signers') {
|
||||
setStep('fields');
|
||||
}
|
||||
|
||||
if (step === 'fields') {
|
||||
setStep('subject');
|
||||
}
|
||||
const documentFlow: Record<EditDocumentStep, DocumentFlowStep> = {
|
||||
signers: {
|
||||
title: 'Add Signers',
|
||||
description: 'Add the people who will sign the document.',
|
||||
stepIndex: 1,
|
||||
onSubmit: () => onAddSignersFormSubmit,
|
||||
},
|
||||
fields: {
|
||||
title: 'Add Fields',
|
||||
description: 'Add all relevant fields for each recipient.',
|
||||
stepIndex: 2,
|
||||
onBackStep: () => setStep('signers'),
|
||||
onSubmit: () => onAddFieldsFormSubmit,
|
||||
},
|
||||
subject: {
|
||||
title: 'Add Subject',
|
||||
description: 'Add the subject and message you wish to send to signers.',
|
||||
stepIndex: 3,
|
||||
onBackStep: () => setStep('fields'),
|
||||
onSubmit: () => onAddSubjectFormSubmit,
|
||||
},
|
||||
};
|
||||
|
||||
const onPreviousStep = () => {
|
||||
if (step === 'fields') {
|
||||
setStep('signers');
|
||||
}
|
||||
|
||||
if (step === 'subject') {
|
||||
setStep('fields');
|
||||
}
|
||||
};
|
||||
const currentDocumentFlow = documentFlow[step];
|
||||
|
||||
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
|
||||
try {
|
||||
@ -72,7 +84,7 @@ export const EditDocumentForm = ({
|
||||
|
||||
router.refresh();
|
||||
|
||||
onNextStep();
|
||||
setStep('fields');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@ -94,7 +106,7 @@ export const EditDocumentForm = ({
|
||||
|
||||
router.refresh();
|
||||
|
||||
onNextStep();
|
||||
setStep('subject');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@ -119,8 +131,6 @@ export const EditDocumentForm = ({
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
|
||||
onNextStep();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@ -144,38 +154,43 @@ export const EditDocumentForm = ({
|
||||
</Card>
|
||||
|
||||
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
||||
{step === 'signers' && (
|
||||
<AddSignersFormPartial
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
document={document}
|
||||
onContinue={onNextStep}
|
||||
onGoBack={onPreviousStep}
|
||||
onSubmit={onAddSignersFormSubmit}
|
||||
<DocumentFlowFormContainer onSubmit={(e) => e.preventDefault()}>
|
||||
<DocumentFlowFormContainerHeader
|
||||
title={currentDocumentFlow.title}
|
||||
description={currentDocumentFlow.description}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === 'fields' && (
|
||||
<AddFieldsFormPartial
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
document={document}
|
||||
onContinue={onNextStep}
|
||||
onGoBack={onPreviousStep}
|
||||
onSubmit={onAddFieldsFormSubmit}
|
||||
/>
|
||||
)}
|
||||
{step === 'signers' && (
|
||||
<AddSignersFormPartial
|
||||
documentFlow={documentFlow.signers}
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
numberOfSteps={Object.keys(documentFlow).length}
|
||||
onSubmit={onAddSignersFormSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === 'subject' && (
|
||||
<AddSubjectFormPartial
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
document={document}
|
||||
onContinue={onNextStep}
|
||||
onGoBack={onPreviousStep}
|
||||
onSubmit={onAddSubjectFormSubmit}
|
||||
/>
|
||||
)}
|
||||
{step === 'fields' && (
|
||||
<AddFieldsFormPartial
|
||||
documentFlow={documentFlow.fields}
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
numberOfSteps={Object.keys(documentFlow).length}
|
||||
onSubmit={onAddFieldsFormSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === 'subject' && (
|
||||
<AddSubjectFormPartial
|
||||
documentFlow={documentFlow.subject}
|
||||
document={document}
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
numberOfSteps={Object.keys(documentFlow).length}
|
||||
onSubmit={onAddSubjectFormSubmit}
|
||||
/>
|
||||
)}
|
||||
</DocumentFlowFormContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -13,9 +13,9 @@ export default function Loading() {
|
||||
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
|
||||
Loading Document...
|
||||
</h1>
|
||||
<div className="mt-8 grid min-h-[80vh] w-full grid-cols-12 gap-x-8">
|
||||
<div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">
|
||||
<div className="dark:bg-background border-border col-span-12 rounded-xl border-2 bg-white/50 p-2 before:rounded-xl lg:col-span-6 xl:col-span-7">
|
||||
<div className="flex min-h-[80vh] flex-col items-center justify-center">
|
||||
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center">
|
||||
<Loader className="text-documenso h-12 w-12 animate-spin" />
|
||||
|
||||
<p className="text-muted-foreground mt-4">Loading document...</p>
|
||||
|
||||
@ -50,7 +50,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
||||
return (
|
||||
<form
|
||||
className={cn(
|
||||
'dark:bg-background border-border bg-widget sticky top-20 flex h-[calc(100vh-6rem)] max-h-screen flex-col rounded-xl border px-4 py-6',
|
||||
'dark:bg-background border-border bg-widget sticky top-20 flex h-full max-h-[80rem] flex-col rounded-xl border px-4 py-6',
|
||||
)}
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
>
|
||||
|
||||
93
packages/lib/client-only/hooks/use-document-element.ts
Normal file
93
packages/lib/client-only/hooks/use-document-element.ts
Normal file
@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
|
||||
export const useDocumentElement = () => {
|
||||
/**
|
||||
* Given a mouse event, find the nearest element found by the provided selector.
|
||||
*/
|
||||
const getPage = (event: MouseEvent, pageSelector: string) => {
|
||||
if (!(event.target instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const target = event.target;
|
||||
|
||||
const $page =
|
||||
target.closest<HTMLElement>(pageSelector) ?? target.querySelector<HTMLElement>(pageSelector);
|
||||
|
||||
if (!$page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $page;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provided a page and a field, calculate the position of the field
|
||||
* as a percentage of the page width and height.
|
||||
*/
|
||||
const getFieldPosition = (page: HTMLElement, field: HTMLElement) => {
|
||||
const {
|
||||
top: pageTop,
|
||||
left: pageLeft,
|
||||
height: pageHeight,
|
||||
width: pageWidth,
|
||||
} = getBoundingClientRect(page);
|
||||
|
||||
const {
|
||||
top: fieldTop,
|
||||
left: fieldLeft,
|
||||
height: fieldHeight,
|
||||
width: fieldWidth,
|
||||
} = getBoundingClientRect(field);
|
||||
|
||||
return {
|
||||
x: ((fieldLeft - pageLeft) / pageWidth) * 100,
|
||||
y: ((fieldTop - pageTop) / pageHeight) * 100,
|
||||
width: (fieldWidth / pageWidth) * 100,
|
||||
height: (fieldHeight / pageHeight) * 100,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a mouse event, determine if the mouse is within the bounds of the
|
||||
* nearest element found by the provided selector.
|
||||
*
|
||||
* @param mouseWidth The artifical width of the mouse.
|
||||
* @param mouseHeight The artifical height of the mouse.
|
||||
*/
|
||||
const isWithinPageBounds = useCallback(
|
||||
(event: MouseEvent, pageSelector: string, mouseWidth = 0, mouseHeight = 0) => {
|
||||
const $page = getPage(event, pageSelector);
|
||||
|
||||
if (!$page) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { top, left, height, width } = $page.getBoundingClientRect();
|
||||
|
||||
const halfMouseWidth = mouseWidth / 2;
|
||||
const halfMouseHeight = mouseHeight / 2;
|
||||
|
||||
if (event.clientY > top + height - halfMouseHeight || event.clientY < top + halfMouseHeight) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.clientX > left + width - halfMouseWidth || event.clientX < left + halfMouseWidth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
getPage,
|
||||
getFieldPosition,
|
||||
isWithinPageBounds,
|
||||
};
|
||||
};
|
||||
@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { VariantProps, cva } from 'class-variance-authority';
|
||||
import { Loader } from 'lucide-react';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
@ -30,17 +31,51 @@ const buttonVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
const loaderVariants = cva('mr-2 animate-spin', {
|
||||
variants: {
|
||||
size: {
|
||||
default: 'h-5 w-5',
|
||||
sm: 'h-4 w-4',
|
||||
lg: 'h-5 w-5',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
|
||||
/**
|
||||
* Will display the loading spinner and disable the button.
|
||||
*/
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
if (asChild) {
|
||||
return (
|
||||
<Slot className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
const showLoader = props.loading === true;
|
||||
const isDisabled = props.disabled || showLoader;
|
||||
|
||||
return (
|
||||
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
<button
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{showLoader && <Loader className={cn('mr-2 animate-spin', loaderVariants({ size }))} />}
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -9,8 +9,9 @@ import { nanoid } from 'nanoid';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { Document, Field, FieldType, Recipient, SendStatus } from '@documenso/prisma/client';
|
||||
import { Field, FieldType, Recipient, SendStatus } from '@documenso/prisma/client';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
@ -26,14 +27,13 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitive
|
||||
|
||||
import { TAddFieldsFormSchema } from './add-fields.types';
|
||||
import {
|
||||
DocumentFlowFormContainer,
|
||||
DocumentFlowFormContainerActions,
|
||||
DocumentFlowFormContainerContent,
|
||||
DocumentFlowFormContainerFooter,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { FieldItem } from './field-item';
|
||||
import { FRIENDLY_FIELD_TYPE } from './types';
|
||||
import { DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types';
|
||||
|
||||
const fontCaveat = Caveat({
|
||||
weight: ['500'],
|
||||
@ -49,20 +49,24 @@ const MIN_HEIGHT_PX = 60;
|
||||
const MIN_WIDTH_PX = 200;
|
||||
|
||||
export type AddFieldsFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
hideRecipients?: boolean;
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
document: Document;
|
||||
onContinue?: () => void;
|
||||
onGoBack?: () => void;
|
||||
numberOfSteps: number;
|
||||
onSubmit: (_data: TAddFieldsFormSchema) => void;
|
||||
};
|
||||
|
||||
export const AddFieldsFormPartial = ({
|
||||
documentFlow,
|
||||
hideRecipients = false,
|
||||
recipients,
|
||||
fields,
|
||||
onGoBack,
|
||||
numberOfSteps,
|
||||
onSubmit,
|
||||
}: AddFieldsFormProps) => {
|
||||
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
@ -99,7 +103,7 @@ export const AddFieldsFormPartial = ({
|
||||
|
||||
const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT;
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
|
||||
const [coords, setCoords] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
@ -110,86 +114,17 @@ export const AddFieldsFormPartial = ({
|
||||
width: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* Given a mouse event, find the nearest pdf page element.
|
||||
*/
|
||||
const getPage = (event: MouseEvent) => {
|
||||
if (!(event.target instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const target = event.target;
|
||||
|
||||
const $page =
|
||||
target.closest<HTMLElement>(PDF_VIEWER_PAGE_SELECTOR) ??
|
||||
target.querySelector<HTMLElement>(PDF_VIEWER_PAGE_SELECTOR);
|
||||
|
||||
if (!$page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $page;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provided a page and a field, calculate the position of the field
|
||||
* as a percentage of the page width and height.
|
||||
*/
|
||||
const getFieldPosition = (page: HTMLElement, field: HTMLElement) => {
|
||||
const {
|
||||
top: pageTop,
|
||||
left: pageLeft,
|
||||
height: pageHeight,
|
||||
width: pageWidth,
|
||||
} = getBoundingClientRect(page);
|
||||
|
||||
const {
|
||||
top: fieldTop,
|
||||
left: fieldLeft,
|
||||
height: fieldHeight,
|
||||
width: fieldWidth,
|
||||
} = getBoundingClientRect(field);
|
||||
|
||||
return {
|
||||
x: ((fieldLeft - pageLeft) / pageWidth) * 100,
|
||||
y: ((fieldTop - pageTop) / pageHeight) * 100,
|
||||
width: (fieldWidth / pageWidth) * 100,
|
||||
height: (fieldHeight / pageHeight) * 100,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a mouse event, determine if the mouse is within the bounds of the
|
||||
* nearest pdf page element.
|
||||
*/
|
||||
const isWithinPageBounds = useCallback((event: MouseEvent) => {
|
||||
const $page = getPage(event);
|
||||
|
||||
if (!$page) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { top, left, height, width } = $page.getBoundingClientRect();
|
||||
|
||||
if (event.clientY > top + height || event.clientY < top) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.clientX > left + width || event.clientX < left) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const onMouseMove = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (!isWithinPageBounds(event)) {
|
||||
setVisible(false);
|
||||
return;
|
||||
}
|
||||
setIsFieldWithinBounds(
|
||||
isWithinPageBounds(
|
||||
event,
|
||||
PDF_VIEWER_PAGE_SELECTOR,
|
||||
fieldBounds.current.width,
|
||||
fieldBounds.current.height,
|
||||
),
|
||||
);
|
||||
|
||||
setVisible(true);
|
||||
setCoords({
|
||||
x: event.clientX - fieldBounds.current.width / 2,
|
||||
y: event.clientY - fieldBounds.current.height / 2,
|
||||
@ -204,9 +139,18 @@ export const AddFieldsFormPartial = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const $page = getPage(event);
|
||||
const $page = getPage(event, PDF_VIEWER_PAGE_SELECTOR);
|
||||
|
||||
if (!$page || !isWithinPageBounds(event)) {
|
||||
if (
|
||||
!$page ||
|
||||
!isWithinPageBounds(
|
||||
event,
|
||||
PDF_VIEWER_PAGE_SELECTOR,
|
||||
fieldBounds.current.width,
|
||||
fieldBounds.current.height,
|
||||
)
|
||||
) {
|
||||
setSelectedField(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -237,10 +181,10 @@ export const AddFieldsFormPartial = ({
|
||||
signerEmail: selectedSigner.email,
|
||||
});
|
||||
|
||||
setVisible(false);
|
||||
setIsFieldWithinBounds(false);
|
||||
setSelectedField(null);
|
||||
},
|
||||
[append, isWithinPageBounds, selectedField, selectedSigner],
|
||||
[append, isWithinPageBounds, selectedField, selectedSigner, getPage],
|
||||
);
|
||||
|
||||
const onFieldResize = useCallback(
|
||||
@ -270,7 +214,7 @@ export const AddFieldsFormPartial = ({
|
||||
pageHeight,
|
||||
});
|
||||
},
|
||||
[localFields, update],
|
||||
[getFieldPosition, localFields, update],
|
||||
);
|
||||
|
||||
const onFieldMove = useCallback(
|
||||
@ -293,7 +237,7 @@ export const AddFieldsFormPartial = ({
|
||||
pageY,
|
||||
});
|
||||
},
|
||||
[localFields, update],
|
||||
[getFieldPosition, localFields, update],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -328,15 +272,18 @@ export const AddFieldsFormPartial = ({
|
||||
}, [recipients]);
|
||||
|
||||
return (
|
||||
<DocumentFlowFormContainer>
|
||||
<DocumentFlowFormContainerContent
|
||||
title="Add Fields"
|
||||
description="Add all relevant fields for each recipient."
|
||||
>
|
||||
<>
|
||||
<DocumentFlowFormContainerContent>
|
||||
<div className="flex flex-col">
|
||||
{selectedField && visible && (
|
||||
{selectedField && (
|
||||
<Card
|
||||
className="border-primary pointer-events-none fixed z-50 cursor-pointer bg-white"
|
||||
className={cn(
|
||||
'pointer-events-none fixed z-50 cursor-pointer bg-white transition-opacity',
|
||||
{
|
||||
'border-primary': isFieldWithinBounds,
|
||||
'opacity-50': !isFieldWithinBounds,
|
||||
},
|
||||
)}
|
||||
style={{
|
||||
top: coords.y,
|
||||
left: coords.x,
|
||||
@ -357,94 +304,100 @@ export const AddFieldsFormPartial = ({
|
||||
disabled={selectedSigner?.email !== field.signerEmail || hasSelectedSignerBeenSent}
|
||||
minHeight={fieldBounds.current.height}
|
||||
minWidth={fieldBounds.current.width}
|
||||
passive={visible && !!selectedField}
|
||||
passive={isFieldWithinBounds && !!selectedField}
|
||||
onResize={(options) => onFieldResize(options, index)}
|
||||
onMove={(options) => onFieldMove(options, index)}
|
||||
onRemove={() => remove(index)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="bg-background text-muted-foreground justify-between font-normal"
|
||||
>
|
||||
{selectedSigner?.email && (
|
||||
<span className="flex-1 truncate text-left">
|
||||
{selectedSigner?.email} ({selectedSigner?.email})
|
||||
</span>
|
||||
)}
|
||||
{!hideRecipients && (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="bg-background text-muted-foreground mb-12 justify-between font-normal"
|
||||
>
|
||||
{selectedSigner?.email && (
|
||||
<span className="flex-1 truncate text-left">
|
||||
{selectedSigner?.email} ({selectedSigner?.email})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!selectedSigner?.email && (
|
||||
<span className="flex-1 truncate text-left">{selectedSigner?.email}</span>
|
||||
)}
|
||||
{!selectedSigner?.email && (
|
||||
<span className="flex-1 truncate text-left">{selectedSigner?.email}</span>
|
||||
)}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput />
|
||||
<CommandEmpty />
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput />
|
||||
<CommandEmpty />
|
||||
|
||||
<CommandGroup>
|
||||
{recipients.map((recipient, index) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
className={cn({
|
||||
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
||||
})}
|
||||
onSelect={() => setSelectedSigner(recipient)}
|
||||
>
|
||||
{recipient.sendStatus !== SendStatus.SENT ? (
|
||||
<Check
|
||||
aria-hidden={recipient !== selectedSigner}
|
||||
className={cn('mr-2 h-4 w-4 flex-shrink-0', {
|
||||
'opacity-0': recipient !== selectedSigner,
|
||||
'opacity-100': recipient === selectedSigner,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="mr-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
This document has already been sent to this recipient. You can no longer
|
||||
edit this recipient.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<CommandGroup>
|
||||
{recipients.map((recipient, index) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
className={cn({
|
||||
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
||||
})}
|
||||
onSelect={() => setSelectedSigner(recipient)}
|
||||
>
|
||||
{recipient.sendStatus !== SendStatus.SENT ? (
|
||||
<Check
|
||||
aria-hidden={recipient !== selectedSigner}
|
||||
className={cn('mr-2 h-4 w-4 flex-shrink-0', {
|
||||
'opacity-0': recipient !== selectedSigner,
|
||||
'opacity-100': recipient === selectedSigner,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="mr-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
This document has already been sent to this recipient. You can no
|
||||
longer edit this recipient.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{recipient.name && (
|
||||
<span className="truncate" title={`${recipient.name} (${recipient.email})`}>
|
||||
{recipient.name} ({recipient.email})
|
||||
</span>
|
||||
)}
|
||||
{recipient.name && (
|
||||
<span
|
||||
className="truncate"
|
||||
title={`${recipient.name} (${recipient.email})`}
|
||||
>
|
||||
{recipient.name} ({recipient.email})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!recipient.name && (
|
||||
<span className="truncate" title={recipient.email}>
|
||||
{recipient.email}
|
||||
</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{!recipient.name && (
|
||||
<span className="truncate" title={recipient.email}>
|
||||
{recipient.email}
|
||||
</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
<div className="-mx-2 mt-8 flex-1 overflow-y-scroll px-2">
|
||||
<div className="mt-4 grid grid-cols-2 gap-x-4 gap-y-8">
|
||||
<div className="-mx-2 flex-1 overflow-y-scroll px-2">
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-8">
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
||||
onClick={() => setSelectedField(FieldType.SIGNATURE)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={() => setSelectedField(FieldType.SIGNATURE)}
|
||||
data-selected={selectedField === FieldType.SIGNATURE ? true : undefined}
|
||||
>
|
||||
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
|
||||
@ -467,7 +420,8 @@ export const AddFieldsFormPartial = ({
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
||||
onClick={() => setSelectedField(FieldType.EMAIL)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={() => setSelectedField(FieldType.EMAIL)}
|
||||
data-selected={selectedField === FieldType.EMAIL ? true : undefined}
|
||||
>
|
||||
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
|
||||
@ -489,7 +443,8 @@ export const AddFieldsFormPartial = ({
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
||||
onClick={() => setSelectedField(FieldType.NAME)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={() => setSelectedField(FieldType.NAME)}
|
||||
data-selected={selectedField === FieldType.NAME ? true : undefined}
|
||||
>
|
||||
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
|
||||
@ -511,7 +466,8 @@ export const AddFieldsFormPartial = ({
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
||||
onClick={() => setSelectedField(FieldType.DATE)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={() => setSelectedField(FieldType.DATE)}
|
||||
data-selected={selectedField === FieldType.DATE ? true : undefined}
|
||||
>
|
||||
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
|
||||
@ -534,15 +490,19 @@ export const AddFieldsFormPartial = ({
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
<DocumentFlowFormContainerFooter>
|
||||
<DocumentFlowFormContainerStep title="Add Fields" step={2} maxStep={3} />
|
||||
<DocumentFlowFormContainerStep
|
||||
title={documentFlow.title}
|
||||
step={documentFlow.stepIndex}
|
||||
maxStep={numberOfSteps}
|
||||
/>
|
||||
|
||||
<DocumentFlowFormContainerActions
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
onGoBackClick={documentFlow.onBackStep}
|
||||
onGoNextClick={() => handleSubmit(onSubmit)()}
|
||||
onGoBackClick={onGoBack}
|
||||
/>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
</DocumentFlowFormContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,40 +2,41 @@
|
||||
|
||||
import React, { useId } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Plus, Trash } from 'lucide-react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||
|
||||
import { Document, Field, Recipient, SendStatus } from '@documenso/prisma/client';
|
||||
import { Field, Recipient, SendStatus } from '@documenso/prisma/client';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
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 } from './add-signers.types';
|
||||
import { TAddSignersFormSchema, ZAddSignersFormSchema } from './add-signers.types';
|
||||
import {
|
||||
DocumentFlowFormContainer,
|
||||
DocumentFlowFormContainerActions,
|
||||
DocumentFlowFormContainerContent,
|
||||
DocumentFlowFormContainerFooter,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { DocumentFlowStep } from './types';
|
||||
|
||||
export type AddSignersFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
document: Document;
|
||||
onContinue?: () => void;
|
||||
onGoBack?: () => void;
|
||||
numberOfSteps: number;
|
||||
onSubmit: (_data: TAddSignersFormSchema) => void;
|
||||
};
|
||||
|
||||
export const AddSignersFormPartial = ({
|
||||
documentFlow,
|
||||
numberOfSteps,
|
||||
recipients,
|
||||
fields: _fields,
|
||||
onGoBack,
|
||||
onSubmit,
|
||||
}: AddSignersFormProps) => {
|
||||
const { toast } = useToast();
|
||||
@ -47,6 +48,7 @@ export const AddSignersFormPartial = ({
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<TAddSignersFormSchema>({
|
||||
resolver: zodResolver(ZAddSignersFormSchema),
|
||||
defaultValues: {
|
||||
signers:
|
||||
recipients.length > 0
|
||||
@ -116,11 +118,8 @@ export const AddSignersFormPartial = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<DocumentFlowFormContainer onSubmit={handleSubmit(onSubmit)}>
|
||||
<DocumentFlowFormContainerContent
|
||||
title="Add Signers"
|
||||
description="Add the people who will sign the document."
|
||||
>
|
||||
<>
|
||||
<DocumentFlowFormContainerContent>
|
||||
<div className="flex w-full flex-col gap-y-4">
|
||||
<AnimatePresence>
|
||||
{signers.map((signer, index) => (
|
||||
@ -205,15 +204,19 @@ export const AddSignersFormPartial = ({
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
<DocumentFlowFormContainerFooter>
|
||||
<DocumentFlowFormContainerStep title="Add Signers" step={1} maxStep={3} />
|
||||
<DocumentFlowFormContainerStep
|
||||
title={documentFlow.title}
|
||||
step={documentFlow.stepIndex}
|
||||
maxStep={numberOfSteps}
|
||||
/>
|
||||
|
||||
<DocumentFlowFormContainerActions
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
onGoBackClick={documentFlow.onBackStep}
|
||||
onGoNextClick={() => handleSubmit(onSubmit)()}
|
||||
onGoBackClick={onGoBack}
|
||||
/>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
</DocumentFlowFormContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -3,35 +3,35 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Document, DocumentStatus, Field, Recipient } from '@documenso/prisma/client';
|
||||
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 { Textarea } from '@documenso/ui/primitives/textarea';
|
||||
|
||||
import { FormErrorMessage } from '~/components/form/form-error-message';
|
||||
|
||||
import { TAddSubjectFormSchema } from './add-subject.types';
|
||||
import {
|
||||
DocumentFlowFormContainer,
|
||||
DocumentFlowFormContainerActions,
|
||||
DocumentFlowFormContainerContent,
|
||||
DocumentFlowFormContainerFooter,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { DocumentFlowStep } from './types';
|
||||
|
||||
export type AddSubjectFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
document: Document;
|
||||
onContinue?: () => void;
|
||||
onGoBack?: () => void;
|
||||
numberOfSteps: number;
|
||||
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
||||
};
|
||||
|
||||
export const AddSubjectFormPartial = ({
|
||||
documentFlow,
|
||||
recipients: _recipients,
|
||||
fields: _fields,
|
||||
document,
|
||||
onGoBack,
|
||||
numberOfSteps,
|
||||
onSubmit,
|
||||
}: AddSubjectFormProps) => {
|
||||
const {
|
||||
@ -48,11 +48,8 @@ export const AddSubjectFormPartial = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<DocumentFlowFormContainer>
|
||||
<DocumentFlowFormContainerContent
|
||||
title="Add Subject"
|
||||
description="Add the subject and message you wish to send to signers."
|
||||
>
|
||||
<>
|
||||
<DocumentFlowFormContainerContent>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div>
|
||||
@ -122,16 +119,20 @@ export const AddSubjectFormPartial = ({
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
<DocumentFlowFormContainerFooter>
|
||||
<DocumentFlowFormContainerStep title="Add Subject" step={3} maxStep={3} />
|
||||
<DocumentFlowFormContainerStep
|
||||
title={documentFlow.title}
|
||||
step={documentFlow.stepIndex}
|
||||
maxStep={numberOfSteps}
|
||||
/>
|
||||
|
||||
<DocumentFlowFormContainerActions
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
goNextLabel={document.status === DocumentStatus.DRAFT ? 'Send' : 'Update'}
|
||||
onGoBackClick={documentFlow.onBackStep}
|
||||
onGoNextClick={() => handleSubmit(onSubmit)()}
|
||||
onGoBackClick={onGoBack}
|
||||
/>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
</DocumentFlowFormContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import React, { HTMLAttributes } from 'react';
|
||||
|
||||
import { Loader } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -21,7 +21,7 @@ export const DocumentFlowFormContainer = ({
|
||||
<form
|
||||
id={id}
|
||||
className={cn(
|
||||
'dark:bg-background border-border bg-widget sticky top-20 flex h-[calc(100vh-6rem)] max-h-screen flex-col rounded-xl border px-4 py-6',
|
||||
'dark:bg-background border-border bg-widget sticky top-20 flex h-full max-h-[80rem] flex-col rounded-xl border px-4 py-6',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@ -31,27 +31,37 @@ export const DocumentFlowFormContainer = ({
|
||||
);
|
||||
};
|
||||
|
||||
export type DocumentFlowFormContainerContentProps = HTMLAttributes<HTMLDivElement> & {
|
||||
export type DocumentFlowFormContainerHeaderProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const DocumentFlowFormContainerContent = ({
|
||||
children,
|
||||
export const DocumentFlowFormContainerHeader = ({
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
...props
|
||||
}: DocumentFlowFormContainerContentProps) => {
|
||||
}: DocumentFlowFormContainerHeaderProps) => {
|
||||
return (
|
||||
<div className={cn('flex flex-1 flex-col', className)} {...props}>
|
||||
<>
|
||||
<h3 className="text-foreground text-2xl font-semibold">{title}</h3>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">{description}</p>
|
||||
|
||||
<hr className="border-border mb-8 mt-4" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export type DocumentFlowFormContainerContentProps = HTMLAttributes<HTMLDivElement> & {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const DocumentFlowFormContainerContent = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: DocumentFlowFormContainerContentProps) => {
|
||||
return (
|
||||
<div className={cn('flex flex-1 flex-col', className)} {...props}>
|
||||
<div className="-mx-2 flex flex-1 flex-col overflow-y-auto px-2">{children}</div>
|
||||
</div>
|
||||
);
|
||||
@ -94,7 +104,9 @@ export const DocumentFlowFormContainerStep = ({
|
||||
</p>
|
||||
|
||||
<div className="bg-muted relative mt-4 h-[2px] rounded-md">
|
||||
<div
|
||||
<motion.div
|
||||
layout="size"
|
||||
layoutId="document-flow-container-step"
|
||||
className="bg-documenso absolute inset-y-0 left-0"
|
||||
style={{
|
||||
width: `${(100 / maxStep) * step}%`,
|
||||
@ -133,20 +145,19 @@ export const DocumentFlowFormContainerActions = ({
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
disabled={disabled || loading || !canGoBack}
|
||||
disabled={disabled || loading || !canGoBack || !onGoBackClick}
|
||||
onClick={onGoBackClick}
|
||||
>
|
||||
{goBackLabel}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
type="submit"
|
||||
className="bg-documenso flex-1"
|
||||
size="lg"
|
||||
disabled={disabled || loading || !canGoNext}
|
||||
onClick={onGoNextClick}
|
||||
>
|
||||
{loading && <Loader className="mr-2 h-5 w-5 animate-spin" />}
|
||||
{goNextLabel}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -91,9 +91,10 @@ export const FieldItem = ({
|
||||
return createPortal(
|
||||
<Rnd
|
||||
key={coords.pageX + coords.pageY + coords.pageHeight + coords.pageWidth}
|
||||
className={cn('absolute z-20', {
|
||||
className={cn(' z-20', {
|
||||
'pointer-events-none': passive,
|
||||
'pointer-events-none z-10 opacity-75': disabled,
|
||||
'pointer-events-none opacity-75': disabled,
|
||||
'z-10': !active || disabled,
|
||||
})}
|
||||
// minHeight={minHeight}
|
||||
// minWidth={minWidth}
|
||||
@ -117,7 +118,7 @@ export const FieldItem = ({
|
||||
>
|
||||
{!disabled && (
|
||||
<button
|
||||
className="text-muted-foreground/50 hover:text-muted-foreground/80 absolute -right-2 -top-2 z-[9999] flex h-8 w-8 items-center justify-center rounded-full border bg-white shadow-[0_0_0_2px_theme(colors.gray.100/70%)]"
|
||||
className="text-muted-foreground/50 hover:text-muted-foreground/80 absolute -right-2 -top-2 z-20 flex h-8 w-8 items-center justify-center rounded-full border bg-white shadow-[0_0_0_2px_theme(colors.gray.100/70%)]"
|
||||
onClick={() => onRemove?.()}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
|
||||
@ -47,3 +47,12 @@ export const FRIENDLY_FIELD_TYPE: Record<FieldType, string> = {
|
||||
[FieldType.EMAIL]: 'Email',
|
||||
[FieldType.NAME]: 'Name',
|
||||
};
|
||||
|
||||
export interface DocumentFlowStep {
|
||||
title: string;
|
||||
description: string;
|
||||
stepIndex: number;
|
||||
onSubmit?: () => void;
|
||||
onBackStep?: () => void;
|
||||
onNextStep?: () => void;
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import { Loader } from 'lucide-react';
|
||||
export const LazyPDFViewer = dynamic(async () => import('./pdf-viewer'), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="dark:bg-background flex min-h-[80vh] flex-col items-center justify-center bg-white/50">
|
||||
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
|
||||
<Loader className="text-documenso h-12 w-12 animate-spin" />
|
||||
|
||||
<p className="text-muted-foreground mt-4">Loading document...</p>
|
||||
|
||||
@ -104,11 +104,13 @@ export const PDFViewer = ({ className, document, onPageClick, ...props }: PDFVie
|
||||
<div ref={$el} className={cn('overflow-hidden', className)} {...props}>
|
||||
<PDFDocument
|
||||
file={document}
|
||||
className="w-full overflow-hidden rounded"
|
||||
className={cn('w-full overflow-hidden rounded', {
|
||||
'h-[80vh] max-h-[60rem]': numPages === 0,
|
||||
})}
|
||||
onLoadSuccess={(d) => onDocumentLoaded(d)}
|
||||
externalLinkTarget="_blank"
|
||||
loading={
|
||||
<div className="dark:bg-background flex min-h-[80vh] flex-col items-center justify-center bg-white/50">
|
||||
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
|
||||
<Loader className="text-documenso h-12 w-12 animate-spin" />
|
||||
|
||||
<p className="text-muted-foreground mt-4">Loading document...</p>
|
||||
|
||||
12
packages/ui/tailwind.config.cjs
Normal file
12
packages/ui/tailwind.config.cjs
Normal file
@ -0,0 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const baseConfig = require('@documenso/tailwind-config');
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
content: [
|
||||
...baseConfig.content,
|
||||
'./primitives/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./lib/**/*.{ts,tsx}',
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user