mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
Merge pull request #21 from ElTimuro/doc-79-recipient-form-validation
Doc-79-recipient-form-validation
This commit is contained in:
@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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,8 +142,14 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
The people who will sign the document.
|
The people who will sign the document.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form
|
||||||
|
onChange={() => {
|
||||||
|
trigger();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ul role="list" className="divide-y divide-gray-200">
|
<ul role="list" className="divide-y divide-gray-200">
|
||||||
{signers.map((item: any, index: number) => (
|
{fields.map((item: any, index: number) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className="px-0 py-4 w-full hover:bg-green-50 border-0 group"
|
className="px-0 py-4 w-full hover:bg-green-50 border-0 group"
|
||||||
@ -134,25 +169,39 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
{...register(`signers.${index}.email`, {
|
||||||
value={item.email}
|
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||||
|
})}
|
||||||
|
defaultValue={item.email}
|
||||||
disabled={item.sendStatus === "SENT" || loading}
|
disabled={item.sendStatus === "SENT" || loading}
|
||||||
onChange={(e) => {
|
|
||||||
const updatedSigners = [...signers];
|
|
||||||
updatedSigners[index].email = e.target.value;
|
|
||||||
setSigners(updatedSigners);
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
item.documentId = props.document.id;
|
if (!errors?.signers?.[index])
|
||||||
createOrUpdateRecipient(item);
|
createOrUpdateRecipient({
|
||||||
|
...formValues[index],
|
||||||
|
documentId: props.document.id,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event: any) => {
|
onKeyDown={(event: any) => {
|
||||||
if (event.key === "Enter")
|
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"
|
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"
|
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>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@ -167,22 +216,26 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
Name (optional)
|
Name (optional)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="text"
|
||||||
name="name"
|
{...register(`signers.${index}.name`)}
|
||||||
value={item.name}
|
defaultValue={item.name}
|
||||||
disabled={item.sendStatus === "SENT" || loading}
|
disabled={item.sendStatus === "SENT" || loading}
|
||||||
onChange={(e) => {
|
|
||||||
const updatedSigners = [...signers];
|
|
||||||
updatedSigners[index].name = e.target.value;
|
|
||||||
setSigners(updatedSigners);
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
item.documentId = props.document.id;
|
if (!errors?.signers?.[index])
|
||||||
createOrUpdateRecipient(item);
|
createOrUpdateRecipient({
|
||||||
|
...formValues[index],
|
||||||
|
documentId: props.document.id,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event: any) => {
|
onKeyDown={(event: any) => {
|
||||||
if (event.key === "Enter")
|
if (
|
||||||
createOrUpdateRecipient(item);
|
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"
|
className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 sm:text-sm outline-none bg-inherit"
|
||||||
placeholder="John Dorian"
|
placeholder="John Dorian"
|
||||||
@ -193,7 +246,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
{item.sendStatus === "NOT_SENT" ? (
|
{item.sendStatus === "NOT_SENT" ? (
|
||||||
<span
|
<span
|
||||||
id="sent_icon"
|
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
|
Not Sent
|
||||||
</span>
|
</span>
|
||||||
@ -205,7 +258,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
<span id="sent_icon">
|
<span id="sent_icon">
|
||||||
<span
|
<span
|
||||||
id="sent_icon"
|
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>{" "}
|
<CheckIcon className="inline h-5 mr-1"></CheckIcon>{" "}
|
||||||
Sent
|
Sent
|
||||||
@ -219,7 +272,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
<span id="read_icon">
|
<span id="read_icon">
|
||||||
<span
|
<span
|
||||||
id="sent_icon"
|
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-2"></CheckIcon>
|
||||||
<CheckIcon className="inline h-5 mr-1"></CheckIcon>
|
<CheckIcon className="inline h-5 mr-1"></CheckIcon>
|
||||||
@ -233,7 +286,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
<span id="signed_icon">
|
<span id="signed_icon">
|
||||||
<span
|
<span
|
||||||
id="sent_icon"
|
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>
|
<CheckBadgeIcon className="inline h-5 mr-1"></CheckBadgeIcon>
|
||||||
Signed
|
Signed
|
||||||
@ -274,14 +327,10 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
!item.id || item.sendStatus === "SENT" || loading
|
!item.id || item.sendStatus === "SENT" || loading
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const signersWithoutIndex = [...signers];
|
const removedItem = { ...fields }[index];
|
||||||
const removedItem = signersWithoutIndex.splice(
|
remove(index);
|
||||||
index,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
setSigners(signersWithoutIndex);
|
|
||||||
deleteRecipient(item)?.catch((err) => {
|
deleteRecipient(item)?.catch((err) => {
|
||||||
setSigners(signersWithoutIndex.concat(removedItem));
|
append(removedItem);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
|
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
|
||||||
@ -301,12 +350,14 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
name: "",
|
name: "",
|
||||||
documentId: props.document.id,
|
documentId: props.document.id,
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
setSigners(signers.concat(res));
|
append(res);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Signer
|
Add Signer
|
||||||
</Button>
|
</Button>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
</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.`}
|
||||||
|
|||||||
@ -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 } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user