Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Real-Time Validation for CreateUserForm and Standardize UI Across Forms #10054

Open
wants to merge 32 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2b476a4
show validations upfront
AdityaJ2305 Jan 19, 2025
d22de89
typo error
AdityaJ2305 Jan 19, 2025
efa1430
cypress fail test
AdityaJ2305 Jan 19, 2025
39d317d
reset cypress file changes
AdityaJ2305 Jan 19, 2025
d0dec71
cypress fail test
AdityaJ2305 Jan 19, 2025
3154f3f
cypress fail test
AdityaJ2305 Jan 19, 2025
52a6e87
cypress fail test
AdityaJ2305 Jan 19, 2025
09668e4
Empty-Commit
AdityaJ2305 Jan 19, 2025
589e3a8
cypress fail test
AdityaJ2305 Jan 19, 2025
7900659
Empty-Commit
AdityaJ2305 Jan 19, 2025
9d62331
reset cypress changes
AdityaJ2305 Jan 19, 2025
b5debd4
coderabbit suggestion
AdityaJ2305 Jan 19, 2025
5e0afac
coderabbit suggestion
AdityaJ2305 Jan 19, 2025
8845799
coderabbit suggestion
AdityaJ2305 Jan 19, 2025
7cf34b5
reset cypress changes
AdityaJ2305 Jan 19, 2025
80704c0
Merge branch 'develop' into show_validations_upfront
AdityaJ2305 Jan 20, 2025
1a61bd3
Merge branch 'develop' into show_validations_upfront
AdityaJ2305 Jan 20, 2025
7ae4d85
Merge branch 'develop' into show_validations_upfront
AdityaJ2305 Jan 20, 2025
0c3db03
Merge branch 'develop' into show_validations_upfront
AdityaJ2305 Jan 20, 2025
7ec3006
Merge branch 'develop' into show_validations_upfront
AdityaJ2305 Jan 20, 2025
787a018
Merge branch 'develop' into show_validations_upfront
AdityaJ2305 Jan 20, 2025
4a3da83
Empty-Commit
AdityaJ2305 Jan 20, 2025
6d7c031
Merge branch 'develop' into show_validations_upfront
AdityaJ2305 Jan 21, 2025
843a24f
Merge branch 'develop' into show_validations_upfront
AdityaJ2305 Jan 21, 2025
75b9c94
css change
AdityaJ2305 Jan 21, 2025
aaba4c7
add default value
AdityaJ2305 Jan 21, 2025
80b89ec
check username only on focus
AdityaJ2305 Jan 21, 2025
1bc5855
Merge branch 'develop' into show_validations_upfront
AdityaJ2305 Jan 22, 2025
c61d916
resolved conflicts
AdityaJ2305 Jan 22, 2025
bc1fbbc
Merge branch 'develop' into show_validations_upfront
AdityaJ2305 Jan 22, 2025
bc069bf
Merge branch 'develop' into show_validations_upfront
AdityaJ2305 Jan 22, 2025
4d7aa7c
Merge branch 'develop' into show_validations_upfront
AdityaJ2305 Jan 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 12 additions & 15 deletions public/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1363,8 +1363,7 @@
"never_logged_in": "Never Logged In",
"new_password": "New Password",
"new_password_confirmation": "Confirm New Password",
"new_password_different_from_old": "Your new password is different from the old password.",
"new_password_same_as_old": "Your new password must not match the old password.",
"new_password_same_as_old": "Your new password <strong>must not match the old password </strong> ",
"new_password_validation": "New password is not valid.",
"new_session": "New Session",
"next_month": "Next month",
Expand Down Expand Up @@ -1498,21 +1497,18 @@
"pain_chart_description": "Mark region and intensity of pain",
"passport_number": "Passport Number",
"password": "Password",
"password_length_met": "It's at least 8 characters long",
"password_length_validation": "Use at least 8 characters",
"password_lowercase_met": "It includes at least one lowercase letter",
"password_lowercase_validation": "Include at least one lowercase letter",
"password_length_validation": "Use at least <strong>8 characters</strong>",
"password_lowercase_validation": "Include at least <strong>one lowercase letter</strong> (a-z)",
"password_mismatch": "Passwords do not match",
"password_number_met": "It includes at least one number.",
"password_number_validation": "Include at least one number.",
"password_number_validation": "Include at least <strong>one number</strong> (0-9)",
"password_required": "Password is required",
"password_reset_failure": "Password Reset Failed",
"password_reset_success": "Password Reset successfully",
"password_sent": "Password Reset Email Sent",
"password_success_message": "All set! Your password is strong",
"password_update_error": "Error while updating password. Try again later.",
"password_updated": "Password updated successfully",
"password_uppercase_met": "It includes at least one uppercase letter.",
"password_uppercase_validation": "Include at least one uppercase letter.",
"password_uppercase_validation": "Include at least <strong>one uppercase letter</strong> (A-Z).",
"passwords_match": "Passwords match.",
"patient": "Patient",
"patient-notes": "Notes",
Expand Down Expand Up @@ -2170,12 +2166,13 @@
"username": "Username",
"username_already_exists": "This username already exists",
"username_available": "Username is available",
"username_characters_validation": "Only lowercase letters, numbers, and . _ - are allowed",
"username_consecutive_validation": "Cannot contain consecutive special characters",
"username_max_length_validation": "Use at most 16 characters",
"username_min_length_validation": "Use at least 4 characters",
"username_characters_validation": "Only <strong>lowercase letters, numbers, and . _ - </strong>are allowed",
"username_consecutive_validation": "Cannot contain <strong>consecutive special characters</strong>",
"username_max_length_validation": "Use at most <strong>16 characters</strong>",
"username_min_length_validation": "Use at least <strong>4 characters</strong>",
"username_not_available": "Username is not available",
"username_start_end_validation": "Must start and end with a letter or number",
"username_start_end_validation": "Must start and end with a <strong>letter</strong> or <strong>number</strong>",
"username_success_message": "All set! Your username is strong",
"username_userdetails_not_found": "Unable to fetch details as username or user details not found",
"username_valid": "Username is valid",
"users": "Users",
Expand Down
74 changes: 31 additions & 43 deletions src/components/Auth/ResetPassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { PasswordInput } from "@/components/ui/input-password";

import { validateRule } from "@/components/Users/UserFormValidations";
import { ValidationHelper } from "@/components/Users/UserFormValidations";

import { LocalStorageKeys } from "@/common/constants";
import { validatePassword } from "@/common/validation";
Expand All @@ -27,9 +27,7 @@ const ResetPassword = (props: ResetPasswordProps) => {
const initErr: any = {};
const [form, setForm] = useState(initForm);
const [errors, setErrors] = useState(initErr);
const [passwordInputInFocus, setPasswordInputInFocus] = useState(false);
const [confirmPasswordInputInFocus, setConfirmPasswordInputInFocus] =
useState(false);
const [isPasswordFieldFocused, setIsPasswordFieldFocused] = useState(false);

const { t } = useTranslation();
const handleChange = (e: any) => {
Expand Down Expand Up @@ -124,40 +122,41 @@ const ResetPassword = (props: ResetPasswordProps) => {
name="password"
placeholder={t("new_password")}
onChange={handleChange}
onFocus={() => setPasswordInputInFocus(true)}
onBlur={() => setPasswordInputInFocus(false)}
onFocus={() => setIsPasswordFieldFocused(true)}
onBlur={() => setIsPasswordFieldFocused(false)}
/>
{errors.password && (
<div className="mt-1 text-red-500 text-xs" data-input-error>
{errors.password}
</div>
)}
{passwordInputInFocus && (
<div className="text-sm mt-2 pl-2 text-secondary-500">
{validateRule(
form.password?.length >= 8,
t("password_length_validation"),
!form.password,
t("password_length_met"),
)}
{validateRule(
form.password !== form.password.toUpperCase(),
t("password_lowercase_validation"),
!form.password,
t("password_lowercase_met"),
)}
{validateRule(
form.password !== form.password.toLowerCase(),
t("password_uppercase_validation"),
!form.password,
t("password_uppercase_met"),
)}
{validateRule(
/\d/.test(form.password),
t("password_number_validation"),
!form.password,
t("password_number_met"),
)}
{isPasswordFieldFocused && (
<div
className="text-small mt-2 pl-2 text-secondary-500"
aria-live="polite"
>
<ValidationHelper
isInputEmpty={!form.password}
successMessage={t("password_success_message")}
validations={[
{
description: "password_length_validation",
fulfilled: form.password?.length >= 8,
},
{
description: "password_lowercase_validation",
fulfilled: /[a-z]/.test(form.password),
},
{
description: "password_uppercase_validation",
fulfilled: /[A-Z]/.test(form.password),
},
{
description: "password_number_validation",
fulfilled: /\d/.test(form.password),
},
]}
/>
</div>
)}
</div>
Expand All @@ -167,23 +166,12 @@ const ResetPassword = (props: ResetPasswordProps) => {
name="confirm"
placeholder={t("confirm_password")}
onChange={handleChange}
onFocus={() => setConfirmPasswordInputInFocus(true)}
onBlur={() => setConfirmPasswordInputInFocus(false)}
/>
{errors.confirm && (
<div className="mt-1 text-red-500 text-xs" data-input-error>
{errors.confirm}
</div>
)}
{confirmPasswordInputInFocus &&
form.confirm.length > 0 &&
form.password.length > 0 &&
validateRule(
form.confirm === form.password,
t("password_mismatch"),
!form.password && form.password.length > 0,
t("password_match"),
)}
</div>
</div>

Expand Down
97 changes: 86 additions & 11 deletions src/components/Users/CreateUserForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
Expand Down Expand Up @@ -28,7 +28,10 @@ import {
SelectValue,
} from "@/components/ui/select";

import { validateRule } from "@/components/Users/UserFormValidations";
import {
ValidationHelper,
validateRule,
} from "@/components/Users/UserFormValidations";

import { GENDER_TYPES } from "@/common/constants";

Expand Down Expand Up @@ -95,13 +98,22 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
resolver: zodResolver(userFormSchema),
defaultValues: {
user_type: "staff",
username: "",
password: "",
c_password: "",
first_name: "",
last_name: "",
email: "",
phone_number: "+91",
alt_phone_number: "+91",
phone_number_is_whatsapp: true,
gender: "male",
},
});

const [isPasswordFieldFocused, setIsPasswordFieldFocused] = useState(false);
const [isUsernameFieldFocused, setIsUsernameFieldFocused] = useState(false);

const userType = form.watch("user_type");
const usernameInput = form.watch("username");
const phoneNumber = form.watch("phone_number");
Expand Down Expand Up @@ -130,14 +142,8 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
errors: { username },
} = form.formState;
const isInitialRender = usernameInput === "";

if (username?.message) {
return validateRule(
false,
username.message,
isInitialRender,
t("username_valid"),
);
return null;
} else if (isUsernameChecking) {
return (
<div className="flex items-center gap-1">
Expand Down Expand Up @@ -261,10 +267,49 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
data-cy="username-input"
placeholder={t("username")}
{...field}
onFocus={() => setIsUsernameFieldFocused(true)}
onBlur={() => setIsUsernameFieldFocused(false)}
/>
</div>
</FormControl>
{renderUsernameFeedback(usernameInput)}
{isUsernameFieldFocused && (
<>
<div
className="text-small mt-2 pl-2 text-secondary-500"
aria-live="polite"
>
<ValidationHelper
isInputEmpty={!field.value}
successMessage={t("username_success_message")}
validations={[
{
description: "username_min_length_validation",
fulfilled: field.value.length >= 4,
},
{
description: "username_max_length_validation",
fulfilled: field.value.length <= 16,
},
{
description: "username_characters_validation",
fulfilled: /^[a-z0-9._-]*$/.test(field.value),
},
{
description: "username_start_end_validation",
fulfilled: /^[a-z0-9].*[a-z0-9]$/.test(field.value),
},
{
description: "username_consecutive_validation",
fulfilled: !/(?:[._-]{2,})/.test(field.value),
},
]}
/>
</div>
<div className="pl-2">
{renderUsernameFeedback(usernameInput)}
</div>
</>
)}
</FormItem>
)}
/>
Expand All @@ -281,9 +326,39 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
data-cy="password-input"
placeholder={t("password")}
{...field}
onFocus={() => setIsPasswordFieldFocused(true)}
onBlur={() => setIsPasswordFieldFocused(false)}
/>
</FormControl>
<FormMessage />
{isPasswordFieldFocused && (
<div
className="text-small mt-2 pl-2 text-secondary-500"
aria-live="polite"
>
<ValidationHelper
isInputEmpty={!field.value}
successMessage={t("password_success_message")}
validations={[
{
description: "password_length_validation",
fulfilled: field.value.length >= 8,
},
{
description: "password_lowercase_validation",
fulfilled: /[a-z]/.test(field.value),
},
{
description: "password_uppercase_validation",
fulfilled: /[A-Z]/.test(field.value),
},
{
description: "password_number_validation",
fulfilled: /\d/.test(field.value),
},
]}
/>
</div>
)}
</FormItem>
)}
/>
Expand Down
50 changes: 50 additions & 0 deletions src/components/Users/UserFormValidations.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Trans } from "react-i18next";

import CareIcon from "@/CAREUI/icons/CareIcon";

import { classNames } from "@/Utils/utils";
Expand All @@ -6,6 +8,54 @@ export type UserType = "doctor" | "nurse" | "staff" | "volunteer";

export type Gender = "male" | "female" | "non_binary" | "transgender";

type Validation = {
description: string;
fulfilled: boolean;
};

type ValidationHelperProps = {
validations: Validation[];
successMessage: string;
isInputEmpty: boolean;
};
export const ValidationHelper = ({
validations,
successMessage,
isInputEmpty,
}: ValidationHelperProps) => {
const unfulfilledValidations = validations.filter(
(validation) => !validation.fulfilled,
);

const allValid = unfulfilledValidations.length === 0 && !isInputEmpty;

return (
<div>
{isInputEmpty &&
validations.map((validation, index) => (
<div key={index} className="text-gray-500 mb-2 text-sm">
<Trans i18nKey={validation.description} />
</div>
))}
{!isInputEmpty &&
!allValid &&
unfulfilledValidations.map((validation, index) => (
<div key={index} className="text-gray-500 mb-2 text-sm">
<Trans i18nKey={validation.description} />
</div>
))}
{allValid && (
<>
<CareIcon icon="l-check-circle" className="text-sm text-green-500" />
<span className="text-green-600 mt-3 ml-1 text-sm">
{successMessage}
</span>
</>
)}
</div>
);
};

export const validateRule = (
isConditionMet: boolean,
initialMessage: JSX.Element | string,
Expand Down
Loading
Loading