Merge pull request #21 from ElTimuro/doc-79-recipient-form-validation

Doc-79-recipient-form-validation
This commit is contained in:
Timur Ercan
2023-03-06 18:28:58 +01:00
committed by GitHub
3 changed files with 259 additions and 204 deletions

View File

@ -14,12 +14,12 @@ type FormValues = {
};
export default function Signup(props: { source: string }) {
const methods = useForm<FormValues>({});
const form = useForm<FormValues>({});
const {
register,
trigger,
formState: { errors, isSubmitting },
} = methods;
} = form;
const handleErrors = async (resp: Response) => {
if (!resp.ok) {
@ -52,7 +52,7 @@ export default function Signup(props: { source: string }) {
)
.catch((err) => {
toast.dismiss();
methods.setError("apiError", { message: err.message });
form.setError("apiError", { message: err.message });
});
};
@ -123,11 +123,11 @@ export default function Signup(props: { source: string }) {
</div>
{renderApiError()}
{renderFormValidation()}
<FormProvider {...methods}>
<FormProvider {...form}>
<form
onSubmit={methods.handleSubmit(signUp)}
onSubmit={form.handleSubmit(signUp)}
onChange={() => {
methods.clearErrors();
form.clearErrors();
trigger();
}}
className="mt-8 space-y-6"
@ -175,7 +175,7 @@ export default function Signup(props: { source: string }) {
<Button
type="submit"
onClick={() => {
methods.clearErrors();
form.clearErrors();
}}
className="sgroup relative flex w-full"
>

View File

@ -12,6 +12,7 @@ import {
PencilSquareIcon,
TrashIcon,
UserPlusIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { getUserFromToken } from "@documenso/lib/server";
import { getDocument } from "@documenso/lib/query";
@ -23,6 +24,16 @@ import {
deleteRecipient,
sendSigningRequests,
} from "@documenso/lib/api";
import {
FormProvider,
useFieldArray,
useForm,
useWatch,
} from "react-hook-form";
type FormValues = {
signers: { id: number; email: string; name: string }[];
};
const RecipientsPage: NextPageWithLayout = (props: any) => {
const title: string =
@ -46,11 +57,28 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
},
];
const [signers, setSigners] = useState(props?.document?.Recipient);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const form = useForm<FormValues>({
defaultValues: { signers: props?.document?.Recipient },
});
const {
register,
trigger,
control,
formState: { errors },
} = form;
const { fields, append, remove } = useFieldArray({
keyName: "dieldArrayId",
name: "signers",
control,
});
const formValues = useWatch({ control, name: "signers" });
const cancelButtonRef = useRef(null);
const hasEmailError = (formValue: any): boolean => {
const index = formValues.findIndex((e) => e.id === formValue.id);
return !!errors?.signers?.[index]?.email;
};
return (
<>
@ -93,9 +121,10 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
setOpen(true);
}}
disabled={
(signers.length || 0) === 0 ||
!signers.some(
(r: any) => r.email && r.sendStatus === "NOT_SENT"
(formValues.length || 0) === 0 ||
!formValues.some(
(r: any) =>
r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
) ||
loading
}
@ -113,8 +142,14 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
The people who will sign the document.
</p>
</div>
<FormProvider {...form}>
<form
onChange={() => {
trigger();
}}
>
<ul role="list" className="divide-y divide-gray-200">
{signers.map((item: any, index: number) => (
{fields.map((item: any, index: number) => (
<li
key={index}
className="px-0 py-4 w-full hover:bg-green-50 border-0 group"
@ -134,25 +169,39 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
</label>
<input
type="email"
name="email"
value={item.email}
{...register(`signers.${index}.email`, {
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
})}
defaultValue={item.email}
disabled={item.sendStatus === "SENT" || loading}
onChange={(e) => {
const updatedSigners = [...signers];
updatedSigners[index].email = e.target.value;
setSigners(updatedSigners);
}}
onBlur={() => {
item.documentId = props.document.id;
createOrUpdateRecipient(item);
if (!errors?.signers?.[index])
createOrUpdateRecipient({
...formValues[index],
documentId: props.document.id,
});
}}
onKeyDown={(event: any) => {
if (event.key === "Enter")
createOrUpdateRecipient(item);
if (!errors?.signers?.[index])
createOrUpdateRecipient({
...formValues[index],
documentId: props.document.id,
});
}}
className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 sm:text-sm outline-none bg-inherit"
placeholder="john.dorian@loremipsum.com"
/>
{errors?.signers?.[index] ? (
<p
className="mt-2 text-sm text-red-600"
id="email-error"
>
<XMarkIcon className="inline h-5" /> Invalid Email
</p>
) : (
""
)}
</div>
<div
className={classNames(
@ -167,22 +216,26 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
Name (optional)
</label>
<input
type="email"
name="name"
value={item.name}
type="text"
{...register(`signers.${index}.name`)}
defaultValue={item.name}
disabled={item.sendStatus === "SENT" || loading}
onChange={(e) => {
const updatedSigners = [...signers];
updatedSigners[index].name = e.target.value;
setSigners(updatedSigners);
}}
onBlur={() => {
item.documentId = props.document.id;
createOrUpdateRecipient(item);
if (!errors?.signers?.[index])
createOrUpdateRecipient({
...formValues[index],
documentId: props.document.id,
});
}}
onKeyDown={(event: any) => {
if (event.key === "Enter")
createOrUpdateRecipient(item);
if (
event.key === "Enter" &&
!errors?.signers?.[index]
)
createOrUpdateRecipient({
...formValues[index],
documentId: props.document.id,
});
}}
className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 sm:text-sm outline-none bg-inherit"
placeholder="John Dorian"
@ -193,7 +246,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
{item.sendStatus === "NOT_SENT" ? (
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-gray-200 px-2 py-0.5 text-xs font-medium text-gray-800"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-gray-200 px-2 py-0.5 text-xs font-medium text-gray-800"
>
Not Sent
</span>
@ -205,7 +258,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
<span id="sent_icon">
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
>
<CheckIcon className="inline h-5 mr-1"></CheckIcon>{" "}
Sent
@ -219,7 +272,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
<span id="read_icon">
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
>
<CheckIcon className="inline h-5 -mr-2"></CheckIcon>
<CheckIcon className="inline h-5 mr-1"></CheckIcon>
@ -233,7 +286,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
<span id="signed_icon">
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"
>
<CheckBadgeIcon className="inline h-5 mr-1"></CheckBadgeIcon>
Signed
@ -274,14 +327,10 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
!item.id || item.sendStatus === "SENT" || loading
}
onClick={() => {
const signersWithoutIndex = [...signers];
const removedItem = signersWithoutIndex.splice(
index,
1
);
setSigners(signersWithoutIndex);
const removedItem = { ...fields }[index];
remove(index);
deleteRecipient(item)?.catch((err) => {
setSigners(signersWithoutIndex.concat(removedItem));
append(removedItem);
});
}}
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
@ -301,12 +350,14 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
name: "",
documentId: props.document.id,
}).then((res) => {
setSigners(signers.concat(res));
append(res);
});
}}
>
Add Signer
</Button>
</form>
</FormProvider>
</div>
</div>
<Transition.Root show={open} as={Fragment}>
@ -357,7 +408,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
<div className="mt-2">
<p className="text-sm text-gray-500">
{`"${props.document.title}" will be sent to ${
signers.filter(
formValues.filter(
(s: any) => s.email && s.sendStatus != "SENT"
).length
} recipients.`}

View File

@ -18,7 +18,11 @@ export const getDocument = async (
userId: user.id,
},
include: {
Recipient: true,
Recipient: {
orderBy: {
id: "asc",
},
},
Field: { include: { Recipient: true, Signature: true } },
},
});