mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: add uninserted field validation
This commit is contained in:
@ -8,8 +8,10 @@ import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import { Field, FieldType } from '@documenso/prisma/client';
|
||||
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types';
|
||||
@ -24,7 +26,6 @@ import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
|
||||
import { FieldToolTip } from '../field/field-tooltip';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
|
||||
import { ZAddSignatureFormSchema } from './add-signature.types';
|
||||
import {
|
||||
@ -89,40 +90,14 @@ export const AddSignatureFormPartial = ({
|
||||
const uninsertedFields = useMemo(() => {
|
||||
const fields = localFields.filter((field) => !field.inserted);
|
||||
|
||||
return fields.sort((a, b) => {
|
||||
if (a.page < b.page) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.page > b.page) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const aTop = a.positionY;
|
||||
const bTop = b.positionY;
|
||||
|
||||
if (aTop < bTop) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (aTop > bTop) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
return sortFieldsByPosition(fields);
|
||||
}, [localFields]);
|
||||
|
||||
const onValidateFields = async (values: TAddSignatureFormSchema) => {
|
||||
setValidateUninsertedFields(true);
|
||||
const isFieldsValid = validateFieldsInserted(localFields);
|
||||
|
||||
const firstUninsertedField = uninsertedFields[0];
|
||||
|
||||
const firstUninsertedFieldElement =
|
||||
firstUninsertedField && document.getElementById(`field-${firstUninsertedField.id}`);
|
||||
|
||||
if (firstUninsertedFieldElement) {
|
||||
firstUninsertedFieldElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
if (!isFieldsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -212,6 +187,24 @@ export const AddSignatureFormPartial = ({
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* When a form value changes, reset all the corresponding fields to be uninserted.
|
||||
*/
|
||||
const onFormValueChange = (fieldType: FieldType) => {
|
||||
setLocalFields((fields) =>
|
||||
fields.map((field) => {
|
||||
if (field.type !== fieldType) {
|
||||
return field;
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
inserted: false,
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<fieldset className="flex h-full flex-col" disabled={form.formState.isSubmitting}>
|
||||
@ -224,7 +217,16 @@ export const AddSignatureFormPartial = ({
|
||||
<FormItem>
|
||||
<FormLabel required>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input className="bg-background" type="email" autoComplete="email" {...field} />
|
||||
<Input
|
||||
className="bg-background"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
{...field}
|
||||
onChange={(value) => {
|
||||
onFormValueChange(FieldType.EMAIL);
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -239,7 +241,14 @@ export const AddSignatureFormPartial = ({
|
||||
<FormItem>
|
||||
<FormLabel required={requireName}>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} />
|
||||
<Input
|
||||
className="bg-background"
|
||||
{...field}
|
||||
onChange={(value) => {
|
||||
onFormValueChange(FieldType.NAME);
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -267,7 +276,11 @@ export const AddSignatureFormPartial = ({
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onBlur={field.onBlur}
|
||||
onChange={(value) => {
|
||||
onFormValueChange(FieldType.SIGNATURE);
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -309,7 +322,6 @@ export const AddSignatureFormPartial = ({
|
||||
return (
|
||||
<SinglePlayerModeCustomTextField
|
||||
onClick={insertField(field)}
|
||||
validateUninsertedField={validateUninsertedFields}
|
||||
key={field.id}
|
||||
field={field}
|
||||
/>
|
||||
@ -318,7 +330,6 @@ export const AddSignatureFormPartial = ({
|
||||
.with(FieldType.SIGNATURE, () => (
|
||||
<SinglePlayerModeSignatureField
|
||||
onClick={insertField(field)}
|
||||
validateUninsertedField={validateUninsertedFields}
|
||||
key={field.id}
|
||||
field={field}
|
||||
/>
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useElementScaleSize } from '@documenso/lib/client-only/hooks/use-element-scale-size';
|
||||
@ -16,92 +15,46 @@ import {
|
||||
} from '@documenso/lib/constants/pdf';
|
||||
import { Field, FieldType } from '@documenso/prisma/client';
|
||||
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
|
||||
export type FieldContainerPortalProps = {
|
||||
field: FieldWithSignature;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
||||
|
||||
export type SinglePlayerModeFieldContainerProps = {
|
||||
field: FieldWithSignature;
|
||||
children: React.ReactNode;
|
||||
validateUninsertedField?: boolean;
|
||||
};
|
||||
|
||||
export type SinglePlayerModeFieldProps<T> = {
|
||||
field: T;
|
||||
onClick?: () => void;
|
||||
validateUninsertedField?: boolean;
|
||||
};
|
||||
|
||||
export function FieldContainerPortal({
|
||||
field,
|
||||
children,
|
||||
className = '',
|
||||
}: FieldContainerPortalProps) {
|
||||
const coords = useFieldPageCoords(field);
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={cn('absolute', className)}
|
||||
style={{
|
||||
top: `${coords.y}px`,
|
||||
left: `${coords.x}px`,
|
||||
height: `${coords.height}px`,
|
||||
width: `${coords.width}px`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
export function SinglePlayerModeFieldCardContainer({
|
||||
field,
|
||||
children,
|
||||
validateUninsertedField = false,
|
||||
}: SinglePlayerModeFieldContainerProps) {
|
||||
return (
|
||||
<FieldContainerPortal field={field}>
|
||||
<motion.div className="h-full w-full" animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
||||
<Card
|
||||
id={`field-${field.id}`}
|
||||
className={cn('bg-background relative z-20 h-full w-full transition-all', {
|
||||
'border-orange-300 ring-1 ring-orange-300': !field.inserted && validateUninsertedField,
|
||||
})}
|
||||
data-inserted={field.inserted ? 'true' : 'false'}
|
||||
<FieldRootContainer field={field}>
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<motion.div
|
||||
key={field.inserted ? 'inserted' : 'not-inserted'}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: 'easeIn',
|
||||
}}
|
||||
>
|
||||
<CardContent className="text-foreground hover:shadow-primary-foreground group flex h-full w-full flex-col items-center justify-center p-2">
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<motion.div
|
||||
key={field.inserted ? 'inserted' : 'not-inserted'}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: 'easeIn',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</FieldContainerPortal>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</FieldRootContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function SinglePlayerModeSignatureField({
|
||||
field,
|
||||
validateUninsertedField,
|
||||
onClick,
|
||||
}: SinglePlayerModeFieldProps<FieldWithSignature>) {
|
||||
const fontVariable = '--font-signature';
|
||||
@ -136,10 +89,7 @@ export function SinglePlayerModeSignatureField({
|
||||
const insertedTypeSignature = field.inserted && field.Signature?.typedSignature;
|
||||
|
||||
return (
|
||||
<SinglePlayerModeFieldCardContainer
|
||||
validateUninsertedField={validateUninsertedField}
|
||||
field={field}
|
||||
>
|
||||
<SinglePlayerModeFieldCardContainer field={field}>
|
||||
{insertedBase64Signature ? (
|
||||
<img
|
||||
src={insertedBase64Signature}
|
||||
@ -171,7 +121,6 @@ export function SinglePlayerModeSignatureField({
|
||||
|
||||
export function SinglePlayerModeCustomTextField({
|
||||
field,
|
||||
validateUninsertedField,
|
||||
onClick,
|
||||
}: SinglePlayerModeFieldProps<Field>) {
|
||||
const fontVariable = '--font-sans';
|
||||
@ -203,10 +152,7 @@ export function SinglePlayerModeCustomTextField({
|
||||
const fontSize = maxFontSize * scalingFactor;
|
||||
|
||||
return (
|
||||
<SinglePlayerModeFieldCardContainer
|
||||
validateUninsertedField={validateUninsertedField}
|
||||
field={field}
|
||||
>
|
||||
<SinglePlayerModeFieldCardContainer field={field}>
|
||||
{field.inserted ? (
|
||||
<p
|
||||
ref={$paragraphEl}
|
||||
|
||||
Reference in New Issue
Block a user