feat: add uninserted field validation

This commit is contained in:
David Nguyen
2023-09-22 12:27:39 +10:00
parent 7eed5c7c96
commit dc49277bf9
8 changed files with 273 additions and 133 deletions

View File

@ -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;