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

View File

@ -12,6 +12,7 @@ import {
PencilSquareIcon, PencilSquareIcon,
TrashIcon, TrashIcon,
UserPlusIcon, UserPlusIcon,
XMarkIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { getUserFromToken } from "@documenso/lib/server"; import { getUserFromToken } from "@documenso/lib/server";
import { getDocument } from "@documenso/lib/query"; import { getDocument } from "@documenso/lib/query";
@ -23,6 +24,16 @@ import {
deleteRecipient, deleteRecipient,
sendSigningRequests, sendSigningRequests,
} from "@documenso/lib/api"; } 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 RecipientsPage: NextPageWithLayout = (props: any) => {
const title: string = 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 [loading, setLoading] = useState(false);
const [open, setOpen] = 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 cancelButtonRef = useRef(null);
const hasEmailError = (formValue: any): boolean => {
const index = formValues.findIndex((e) => e.id === formValue.id);
return !!errors?.signers?.[index]?.email;
};
return ( return (
<> <>
@ -93,9 +121,10 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
setOpen(true); setOpen(true);
}} }}
disabled={ disabled={
(signers.length || 0) === 0 || (formValues.length || 0) === 0 ||
!signers.some( !formValues.some(
(r: any) => r.email && r.sendStatus === "NOT_SENT" (r: any) =>
r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
) || ) ||
loading loading
} }
@ -113,200 +142,222 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
The people who will sign the document. The people who will sign the document.
</p> </p>
</div> </div>
<ul role="list" className="divide-y divide-gray-200"> <FormProvider {...form}>
{signers.map((item: any, index: number) => ( <form
<li onChange={() => {
key={index} trigger();
className="px-0 py-4 w-full hover:bg-green-50 border-0 group" }}
> >
<div id="container" className="flex w-full"> <ul role="list" className="divide-y divide-gray-200">
<div {fields.map((item: any, index: number) => (
className={classNames( <li
"ml-3 w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon", key={index}
item.sendStatus === "SENT" ? "bg-gray-100" : "" className="px-0 py-4 w-full hover:bg-green-50 border-0 group"
)}
> >
<label <div id="container" className="flex w-full">
htmlFor="name" <div
className="block text-xs font-medium text-gray-900" className={classNames(
> "ml-3 w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
Email item.sendStatus === "SENT" ? "bg-gray-100" : ""
</label> )}
<input >
type="email" <label
name="email" htmlFor="name"
value={item.email} className="block text-xs font-medium text-gray-900"
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);
}}
onKeyDown={(event: any) => {
if (event.key === "Enter")
createOrUpdateRecipient(item);
}}
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"
/>
</div>
<div
className={classNames(
"ml-3 w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
item.sendStatus === "SENT" ? "bg-gray-100" : ""
)}
>
<label
htmlFor="name"
className="block text-xs font-medium text-gray-900"
>
Name (optional)
</label>
<input
type="email"
name="name"
value={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);
}}
onKeyDown={(event: any) => {
if (event.key === "Enter")
createOrUpdateRecipient(item);
}}
className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 sm:text-sm outline-none bg-inherit"
placeholder="John Dorian"
/>
</div>
<div className="ml-auto flex">
<div key={item.id}>
{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"
> >
Not Sent Email
</span> </label>
) : ( <input
"" type="email"
)} {...register(`signers.${index}.email`, {
{item.sendStatus === "SENT" && pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
item.readStatus !== "OPENED" ? ( })}
<span id="sent_icon"> defaultValue={item.email}
<span disabled={item.sendStatus === "SENT" || loading}
id="sent_icon" onBlur={() => {
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800" if (!errors?.signers?.[index])
createOrUpdateRecipient({
...formValues[index],
documentId: props.document.id,
});
}}
onKeyDown={(event: any) => {
if (event.key === "Enter")
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"
> >
<CheckIcon className="inline h-5 mr-1"></CheckIcon>{" "} <XMarkIcon className="inline h-5" /> Invalid Email
Sent </p>
</span> ) : (
</span> ""
) : ( )}
"" </div>
)} <div
{item.readStatus === "OPENED" && className={classNames(
item.signingStatus === "NOT_SIGNED" ? ( "ml-3 w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
<span id="read_icon"> item.sendStatus === "SENT" ? "bg-gray-100" : ""
<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" <label
> htmlFor="name"
<CheckIcon className="inline h-5 -mr-2"></CheckIcon> className="block text-xs font-medium text-gray-900"
<CheckIcon className="inline h-5 mr-1"></CheckIcon> >
Seen Name (optional)
</span> </label>
</span> <input
) : ( type="text"
"" {...register(`signers.${index}.name`)}
)} defaultValue={item.name}
{item.signingStatus === "SIGNED" ? ( disabled={item.sendStatus === "SENT" || loading}
<span id="signed_icon"> onBlur={() => {
<span if (!errors?.signers?.[index])
id="sent_icon" createOrUpdateRecipient({
className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800" ...formValues[index],
> documentId: props.document.id,
<CheckBadgeIcon className="inline h-5 mr-1"></CheckBadgeIcon> });
Signed }}
</span> onKeyDown={(event: any) => {
</span> 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"
/>
</div>
<div className="ml-auto flex">
<div key={item.id}>
{item.sendStatus === "NOT_SENT" ? (
<span
id="sent_icon"
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>
) : (
""
)}
{item.sendStatus === "SENT" &&
item.readStatus !== "OPENED" ? (
<span id="sent_icon">
<span
id="sent_icon"
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
</span>
</span>
) : (
""
)}
{item.readStatus === "OPENED" &&
item.signingStatus === "NOT_SIGNED" ? (
<span id="read_icon">
<span
id="sent_icon"
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>
Seen
</span>
</span>
) : (
""
)}
{item.signingStatus === "SIGNED" ? (
<span id="signed_icon">
<span
id="sent_icon"
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
</span>
</span>
) : (
""
)}
</div>
</div>
<div className="ml-auto flex mr-1">
<IconButton
icon={PaperAirplaneIcon}
disabled={
!item.id ||
item.sendStatus !== "SENT" ||
item.signingStatus === "SIGNED" ||
loading
}
color="secondary"
className="mr-4 h-9 my-auto"
onClick={() => {
if (confirm("Resend this signing request?")) {
setLoading(true);
sendSigningRequests(props.document, [
item.id,
]).finally(() => {
setLoading(false);
});
}
}}
>
Resend
</IconButton>
<IconButton
icon={TrashIcon}
disabled={
!item.id || item.sendStatus === "SENT" || loading
}
onClick={() => {
const removedItem = { ...fields }[index];
remove(index);
deleteRecipient(item)?.catch((err) => {
append(removedItem);
});
}}
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
/>
</div>
</div> </div>
</div> </li>
<div className="ml-auto flex mr-1"> ))}
<IconButton </ul>
icon={PaperAirplaneIcon} <Button
disabled={ icon={UserPlusIcon}
!item.id || className="mt-3"
item.sendStatus !== "SENT" || onClick={() => {
item.signingStatus === "SIGNED" || createOrUpdateRecipient({
loading id: "",
} email: "",
color="secondary" name: "",
className="mr-4 h-9 my-auto" documentId: props.document.id,
onClick={() => { }).then((res) => {
if (confirm("Resend this signing request?")) { append(res);
setLoading(true); });
sendSigningRequests(props.document, [ }}
item.id, >
]).finally(() => { Add Signer
setLoading(false); </Button>
}); </form>
} </FormProvider>
}}
>
Resend
</IconButton>
<IconButton
icon={TrashIcon}
disabled={
!item.id || item.sendStatus === "SENT" || loading
}
onClick={() => {
const signersWithoutIndex = [...signers];
const removedItem = signersWithoutIndex.splice(
index,
1
);
setSigners(signersWithoutIndex);
deleteRecipient(item)?.catch((err) => {
setSigners(signersWithoutIndex.concat(removedItem));
});
}}
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
/>
</div>
</div>
</li>
))}
</ul>
<Button
icon={UserPlusIcon}
className="mt-3"
onClick={() => {
createOrUpdateRecipient({
id: "",
email: "",
name: "",
documentId: props.document.id,
}).then((res) => {
setSigners(signers.concat(res));
});
}}
>
Add Signer
</Button>
</div> </div>
</div> </div>
<Transition.Root show={open} as={Fragment}> <Transition.Root show={open} as={Fragment}>
@ -357,7 +408,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{`"${props.document.title}" will be sent to ${ {`"${props.document.title}" will be sent to ${
signers.filter( formValues.filter(
(s: any) => s.email && s.sendStatus != "SENT" (s: any) => s.email && s.sendStatus != "SENT"
).length ).length
} recipients.`} } recipients.`}

View File

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