diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/web/src/app/(signing)/sign/[token]/form.tsx
index e18571e33..6a03d43f8 100644
--- a/apps/web/src/app/(signing)/sign/[token]/form.tsx
+++ b/apps/web/src/app/(signing)/sign/[token]/form.tsx
@@ -1,12 +1,15 @@
'use client';
+import { useMemo, useState } from 'react';
+
import { useRouter } from 'next/navigation';
-import { Loader } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
+import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import { Document, Field, Recipient } from '@documenso/prisma/client';
+import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@@ -27,15 +30,22 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
+ const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
+
const {
handleSubmit,
formState: { isSubmitting },
} = useForm();
- const isComplete = fields.every((f) => f.inserted);
+ const uninsertedFields = useMemo(() => {
+ return sortFieldsByPosition(fields.filter((field) => !field.inserted));
+ }, [fields]);
const onFormSubmit = async () => {
- if (!isComplete) {
+ setValidateUninsertedFields(true);
+ const isFieldsValid = validateFieldsInserted(fields);
+
+ if (!isFieldsValid) {
return;
}
@@ -54,7 +64,16 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
)}
onSubmit={handleSubmit(onFormSubmit)}
>
-
+ {validateUninsertedFields && uninsertedFields[0] && (
+
+ Click to insert field
+
+ )}
+
+
-
+
);
};
diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx b/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx
index 749ab660f..72e4e7a70 100644
--- a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx
+++ b/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx
@@ -2,10 +2,8 @@
import React from 'react';
-import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
-import { cn } from '@documenso/ui/lib/utils';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
+import { FieldRootContainer } from '@documenso/ui/components/field/field';
export type SignatureFieldProps = {
field: FieldWithSignature;
@@ -22,8 +20,6 @@ export const SigningFieldContainer = ({
onRemove,
children,
}: SignatureFieldProps) => {
- const coords = useFieldPageCoords(field);
-
const onSignFieldClick = async () => {
if (field.inserted) {
return;
@@ -41,40 +37,21 @@ export const SigningFieldContainer = ({
};
return (
-
-
-
+ {!field.inserted && !loading && (
+
+ )}
+
+ {field.inserted && !loading && (
+
- {!field.inserted && !loading && (
-
- )}
+ Remove
+
+ )}
- {field.inserted && !loading && (
-
- Remove
-
- )}
-
- {children}
-
-
-
+ {children}
+
);
};
diff --git a/packages/lib/utils/fields.ts b/packages/lib/utils/fields.ts
new file mode 100644
index 000000000..b88fed3e9
--- /dev/null
+++ b/packages/lib/utils/fields.ts
@@ -0,0 +1,41 @@
+import { Field } from '@documenso/prisma/client';
+
+/**
+ * Sort the fields by the Y position on the document.
+ */
+export const sortFieldsByPosition = (fields: Field[]): Field[] => {
+ const clonedFields: Field[] = JSON.parse(JSON.stringify(fields));
+
+ // Sort by page first, then position on page second.
+ return clonedFields.sort((a, b) => a.page - b.page || Number(a.positionY) - Number(b.positionY));
+};
+
+/**
+ * Validate whether all the provided fields are inserted.
+ *
+ * If there are any non-inserted fields it will be highlighted and scrolled into view.
+ *
+ * @returns `true` if all fields are inserted, `false` otherwise.
+ */
+export const validateFieldsInserted = (fields: Field[]): boolean => {
+ const fieldCardElements = document.getElementsByClassName('field-card-container');
+
+ // Attach validate attribute on all fields.
+ Array.from(fieldCardElements).forEach((element) => {
+ element.setAttribute('data-validate', 'true');
+ });
+
+ const uninsertedFields = sortFieldsByPosition(fields.filter((field) => !field.inserted));
+
+ const firstUninsertedField = uninsertedFields[0];
+
+ const firstUninsertedFieldElement =
+ firstUninsertedField && document.getElementById(`field-${firstUninsertedField.id}`);
+
+ if (firstUninsertedFieldElement) {
+ firstUninsertedFieldElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ return false;
+ }
+
+ return uninsertedFields.length === 0;
+};
diff --git a/packages/ui/primitives/field/field-tooltip.tsx b/packages/ui/components/field/field-tooltip.tsx
similarity index 92%
rename from packages/ui/primitives/field/field-tooltip.tsx
rename to packages/ui/components/field/field-tooltip.tsx
index c2fa9c580..446b14d2d 100644
--- a/packages/ui/primitives/field/field-tooltip.tsx
+++ b/packages/ui/components/field/field-tooltip.tsx
@@ -39,7 +39,7 @@ export function FieldToolTip({ children, color, className = '', field }: FieldTo
return createPortal(
-
+
{children}
diff --git a/packages/ui/components/field/field.tsx b/packages/ui/components/field/field.tsx
new file mode 100644
index 000000000..054cc6376
--- /dev/null
+++ b/packages/ui/components/field/field.tsx
@@ -0,0 +1,90 @@
+'use client';
+
+import React, { useEffect, useState } from 'react';
+
+import { createPortal } from 'react-dom';
+
+import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
+import { Field } from '@documenso/prisma/client';
+import { cn } from '@documenso/ui/lib/utils';
+import { Card, CardContent } from '@documenso/ui/primitives/card';
+
+export type FieldRootContainerProps = {
+ field: Field;
+ children: React.ReactNode;
+};
+
+export type FieldContainerPortalProps = {
+ field: Field;
+ className?: string;
+ children: React.ReactNode;
+};
+
+export function FieldContainerPortal({
+ field,
+ children,
+ className = '',
+}: FieldContainerPortalProps) {
+ const coords = useFieldPageCoords(field);
+
+ return createPortal(
+
+ {children}
+
,
+ document.body,
+ );
+}
+
+export function FieldRootContainer({ field, children }: FieldContainerPortalProps) {
+ const [isValidating, setIsValidating] = useState(false);
+
+ const ref = React.useRef(null);
+
+ useEffect(() => {
+ if (!ref.current) {
+ return;
+ }
+
+ const observer = new MutationObserver((_mutations) => {
+ if (ref.current) {
+ setIsValidating(ref.current.getAttribute('data-validate') === 'true');
+ }
+ });
+
+ observer.observe(ref.current, {
+ attributes: true,
+ });
+
+ return () => {
+ observer.disconnect();
+ };
+ }, []);
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/packages/ui/primitives/document-flow/add-signature.tsx b/packages/ui/primitives/document-flow/add-signature.tsx
index 98164a506..aed252083 100644
--- a/packages/ui/primitives/document-flow/add-signature.tsx
+++ b/packages/ui/primitives/document-flow/add-signature.tsx
@@ -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 (