mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 16:23:06 +10:00
feat: add uninserted field validation
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { DateTime } from 'luxon';
|
||||
@ -8,7 +8,7 @@ import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
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';
|
||||
@ -24,6 +24,7 @@ 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 {
|
||||
@ -50,6 +51,8 @@ export const AddSignatureFormPartial = ({
|
||||
requireName = false,
|
||||
requireSignature = true,
|
||||
}: AddSignatureFormProps) => {
|
||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||
|
||||
// Refined schema which takes into account whether to allow an empty name or signature.
|
||||
const refinedSchema = ZAddSignatureFormSchema.superRefine((val, ctx) => {
|
||||
if (requireName && val.name.length === 0) {
|
||||
@ -81,72 +84,101 @@ export const AddSignatureFormPartial = ({
|
||||
/**
|
||||
* A local copy of the provided fields to modify.
|
||||
*/
|
||||
const [localFields, setLocalFields] = useState(
|
||||
fields.map((field) => {
|
||||
let customText = field.customText;
|
||||
const [localFields, setLocalFields] = useState<Field[]>(JSON.parse(JSON.stringify(fields)));
|
||||
|
||||
if (field.type === FieldType.DATE) {
|
||||
customText = DateTime.now().toFormat('yyyy-MM-dd hh:mm a');
|
||||
const uninsertedFields = useMemo(() => {
|
||||
const fields = localFields.filter((field) => !field.inserted);
|
||||
|
||||
return fields.sort((a, b) => {
|
||||
if (a.page < b.page) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const inserted = match(field.type)
|
||||
.with(FieldType.DATE, () => true)
|
||||
.with(FieldType.NAME, () => form.getValues('name').length > 0)
|
||||
.with(FieldType.EMAIL, () => form.getValues('email').length > 0)
|
||||
.with(FieldType.SIGNATURE, () => form.getValues('signature').length > 0)
|
||||
.otherwise(() => true);
|
||||
if (a.page > b.page) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return { ...field, inserted, customText };
|
||||
}),
|
||||
);
|
||||
const aTop = a.positionY;
|
||||
const bTop = b.positionY;
|
||||
|
||||
const onEmailInputBlur = () => {
|
||||
setLocalFields((prev) =>
|
||||
prev.map((field) => {
|
||||
if (field.type !== FieldType.EMAIL) {
|
||||
return field;
|
||||
}
|
||||
if (aTop < bTop) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const value = form.getValues('email');
|
||||
if (aTop > bTop) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
customText: value,
|
||||
inserted: value.length > 0,
|
||||
};
|
||||
}),
|
||||
);
|
||||
return 0;
|
||||
});
|
||||
}, [localFields]);
|
||||
|
||||
const onValidateFields = async (values: TAddSignatureFormSchema) => {
|
||||
setValidateUninsertedFields(true);
|
||||
|
||||
const firstUninsertedField = uninsertedFields[0];
|
||||
|
||||
const firstUninsertedFieldElement =
|
||||
firstUninsertedField && document.getElementById(`field-${firstUninsertedField.id}`);
|
||||
|
||||
if (firstUninsertedFieldElement) {
|
||||
firstUninsertedFieldElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
return;
|
||||
}
|
||||
|
||||
await onSubmit(values);
|
||||
};
|
||||
|
||||
const onNameInputBlur = () => {
|
||||
setLocalFields((prev) =>
|
||||
prev.map((field) => {
|
||||
if (field.type !== FieldType.NAME) {
|
||||
return field;
|
||||
}
|
||||
/**
|
||||
* Validates whether the corresponding form for a given field type is valid.
|
||||
*
|
||||
* @returns `true` if the form associated with the provided field is valid, `false` otherwise.
|
||||
*/
|
||||
const validateFieldForm = async (fieldType: Field['type']): Promise<boolean> => {
|
||||
if (fieldType === FieldType.SIGNATURE) {
|
||||
await form.trigger('signature');
|
||||
return !form.formState.errors.signature;
|
||||
}
|
||||
|
||||
const value = form.getValues('name');
|
||||
if (fieldType === FieldType.NAME) {
|
||||
await form.trigger('name');
|
||||
return !form.formState.errors.name;
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
customText: value,
|
||||
inserted: value.length > 0,
|
||||
};
|
||||
}),
|
||||
);
|
||||
if (fieldType === FieldType.EMAIL) {
|
||||
await form.trigger('email');
|
||||
return !form.formState.errors.email;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const onSignatureInputChange = (value: string) => {
|
||||
setLocalFields((prev) =>
|
||||
prev.map((field) => {
|
||||
if (field.type !== FieldType.SIGNATURE) {
|
||||
return field;
|
||||
}
|
||||
/**
|
||||
* Insert the corresponding form value into a given field.
|
||||
*/
|
||||
const insertFormValueIntoField = (field: Field) => {
|
||||
return match(field.type)
|
||||
.with(FieldType.DATE, () => ({
|
||||
...field,
|
||||
customText: DateTime.now().toFormat('yyyy-MM-dd hh:mm a'),
|
||||
inserted: true,
|
||||
}))
|
||||
.with(FieldType.EMAIL, () => ({
|
||||
...field,
|
||||
customText: form.getValues('email'),
|
||||
inserted: true,
|
||||
}))
|
||||
.with(FieldType.NAME, () => ({
|
||||
...field,
|
||||
customText: form.getValues('name'),
|
||||
inserted: true,
|
||||
}))
|
||||
.with(FieldType.SIGNATURE, () => {
|
||||
const value = form.getValues('signature');
|
||||
|
||||
return {
|
||||
...field,
|
||||
value: value ?? '',
|
||||
inserted: true,
|
||||
value,
|
||||
Signature: {
|
||||
id: -1,
|
||||
recipientId: -1,
|
||||
@ -155,7 +187,27 @@ export const AddSignatureFormPartial = ({
|
||||
signatureImageAsBase64: value,
|
||||
typedSignature: null,
|
||||
},
|
||||
inserted: true,
|
||||
};
|
||||
})
|
||||
.otherwise(() => {
|
||||
throw new Error('Unsupported field');
|
||||
});
|
||||
};
|
||||
|
||||
const insertField = (field: Field) => async () => {
|
||||
const isFieldFormValid = await validateFieldForm(field.type);
|
||||
if (!isFieldFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalFields((prev) =>
|
||||
prev.map((prevField) => {
|
||||
if (prevField.id !== field.id) {
|
||||
return prevField;
|
||||
}
|
||||
|
||||
return insertFormValueIntoField(field);
|
||||
}),
|
||||
);
|
||||
};
|
||||
@ -172,16 +224,7 @@ export const AddSignatureFormPartial = ({
|
||||
<FormItem>
|
||||
<FormLabel required>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="bg-background"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
{...field}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
onEmailInputBlur();
|
||||
}}
|
||||
/>
|
||||
<Input className="bg-background" type="email" autoComplete="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -196,14 +239,7 @@ export const AddSignatureFormPartial = ({
|
||||
<FormItem>
|
||||
<FormLabel required={requireName}>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="bg-background"
|
||||
{...field}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
onNameInputBlur();
|
||||
}}
|
||||
/>
|
||||
<Input className="bg-background" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -231,10 +267,7 @@ export const AddSignatureFormPartial = ({
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
defaultValue={field.value}
|
||||
onChange={(value) => {
|
||||
field.onChange(value ?? '');
|
||||
onSignatureInputChange(value ?? '');
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -258,19 +291,37 @@ export const AddSignatureFormPartial = ({
|
||||
loading={form.formState.isSubmitting}
|
||||
disabled={form.formState.isSubmitting}
|
||||
onGoBackClick={documentFlow.onBackStep}
|
||||
onGoNextClick={async () => await form.handleSubmit(onSubmit)()}
|
||||
onGoNextClick={form.handleSubmit(onValidateFields)}
|
||||
/>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
</fieldset>
|
||||
|
||||
{validateUninsertedFields && uninsertedFields[0] && (
|
||||
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
||||
Click to insert field
|
||||
</FieldToolTip>
|
||||
)}
|
||||
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{localFields.map((field) =>
|
||||
match(field.type)
|
||||
.with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, () => {
|
||||
return <SinglePlayerModeCustomTextField key={field.id} field={field} />;
|
||||
return (
|
||||
<SinglePlayerModeCustomTextField
|
||||
onClick={insertField(field)}
|
||||
validateUninsertedField={validateUninsertedFields}
|
||||
key={field.id}
|
||||
field={field}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.with(FieldType.SIGNATURE, () => (
|
||||
<SinglePlayerModeSignatureField key={field.id} field={field} />
|
||||
<SinglePlayerModeSignatureField
|
||||
onClick={insertField(field)}
|
||||
validateUninsertedField={validateUninsertedFields}
|
||||
key={field.id}
|
||||
field={field}
|
||||
/>
|
||||
))
|
||||
.otherwise(() => {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user