diff --git a/.github/workflows/label-merge-conflict.yml b/.github/workflows/label-merge-conflict.yml index d538cbae14b..8c0b06e5baa 100644 --- a/.github/workflows/label-merge-conflict.yml +++ b/.github/workflows/label-merge-conflict.yml @@ -16,7 +16,7 @@ jobs: if: github.repository == 'ohcnetwork/care_fe' runs-on: ubuntu-24.04-arm steps: - - uses: prince-chrismc/label-merge-conflicts-action@v2 + - uses: prince-chrismc/label-merge-conflicts-action@v3 with: conflict_label_name: "merge conflict" github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6eb50339b12..9f949652b15 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ permissions: jobs: release: name: Release on Push - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: diff --git a/package-lock.json b/package-lock.json index 64631cc64f9..d457f3b8f58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16684,9 +16684,9 @@ } }, "node_modules/sonner": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.2.tgz", - "integrity": "sha512-zMbseqjrOzQD1a93lxahm+qMGxWovdMxBlkTbbnZdNqVLt4j+amF9PQxUCL32WfztOFt9t9ADYkejAL3jF9iNA==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.3.tgz", + "integrity": "sha512-KXLWQfyR6AHpYZuQk8eO8fCbZSJY3JOpgsu/tbGc++jgPjj8JsR1ZpO8vFhqR/OxvWMQCSAmnSShY0gr4FPqHg==", "license": "MIT", "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", diff --git a/public/locale/en.json b/public/locale/en.json index decf07e84a6..b80f7d59a7b 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -949,6 +949,7 @@ "external_identifier": "External Identifier", "facilities": "Facilities", "facility": "Facility", + "facility_actions_menu": "Facility action menu", "facility_added_successfully": "Facility created successfully", "facility_consent_requests_page_title": "Patient Consent List", "facility_count_one": "{{count}} Facility", @@ -1294,8 +1295,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 must not match the old password ", "new_password_validation": "New password is not valid.", "new_session": "New Session", "next_month": "Next month", @@ -1440,21 +1440,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 8 characters", + "password_lowercase_validation": "Include at least one lowercase letter (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 one number (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 one uppercase letter (A-Z).", "passwords_match": "Passwords match.", "patient": "Patient", "patient-notes": "Notes", @@ -2135,12 +2132,14 @@ "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 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_not_available": "Username is not available", - "username_start_end_validation": "Must start and end with a letter or number", + "username_not_valid": "username is not valid", + "username_start_end_validation": "Must start and end with a letter or number", + "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", diff --git a/src/components/Auth/ResetPassword.tsx b/src/components/Auth/ResetPassword.tsx index 273e5486140..1f75e5a1ff6 100644 --- a/src/components/Auth/ResetPassword.tsx +++ b/src/components/Auth/ResetPassword.tsx @@ -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"; @@ -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) => { @@ -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 && (
{errors.password}
)} - {passwordInputInFocus && ( -
- {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 && ( +
+ = 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), + }, + ]} + />
)}
@@ -167,23 +166,12 @@ const ResetPassword = (props: ResetPasswordProps) => { name="confirm" placeholder={t("confirm_password")} onChange={handleChange} - onFocus={() => setConfirmPasswordInputInFocus(true)} - onBlur={() => setConfirmPasswordInputInFocus(false)} /> {errors.confirm && (
{errors.confirm}
)} - {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"), - )} diff --git a/src/components/Facility/FacilityHome.tsx b/src/components/Facility/FacilityHome.tsx index e6cddee5a70..5cee02880c0 100644 --- a/src/components/Facility/FacilityHome.tsx +++ b/src/components/Facility/FacilityHome.tsx @@ -1,4 +1,10 @@ import careConfig from "@careConfig"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@radix-ui/react-tooltip"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Hospital, MapPin, MoreVertical, Settings } from "lucide-react"; import { navigate } from "raviger"; @@ -219,32 +225,44 @@ export const FacilityHome = ({ facilityId }: Props) => { )}
-
-
-
+
+
+
-
-

- {facilityData?.name} -

+
+ + + +

+ {facilityData?.name} +

+
+ +

+ {facilityData?.name} +

+
+
+
- + - + {hasPermissionToEditCoverImage && ( {
-
+
diff --git a/src/components/Files/FilesTab.tsx b/src/components/Files/FilesTab.tsx index 27779acec8c..a4a118d8792 100644 --- a/src/components/Files/FilesTab.tsx +++ b/src/components/Files/FilesTab.tsx @@ -595,7 +595,7 @@ const FileUploadDialog = ({ {fileUpload.files.length > 1 ? t("upload_files") : t("upload_file")} -
+
{isPdf ? ( <> {fileUpload.files.map((file, index) => ( diff --git a/src/components/Resource/ResourceBoard.tsx b/src/components/Resource/ResourceBoard.tsx index 0ec92836de3..b048c3c6cd4 100644 --- a/src/components/Resource/ResourceBoard.tsx +++ b/src/components/Resource/ResourceBoard.tsx @@ -43,7 +43,7 @@ export default function BoardView() { cacheBlacklist: ["title"], }); const [boardFilter, setBoardFilter] = useState(ACTIVE); - // eslint-disable-next-line + const appliedFilters = formatFilter(qParams); const { t } = useTranslation(); diff --git a/src/components/Users/UserForm.tsx b/src/components/Users/UserForm.tsx index 2211fbaf020..d5656c89694 100644 --- a/src/components/Users/UserForm.tsx +++ b/src/components/Users/UserForm.tsx @@ -29,7 +29,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"; import { GENDERS } from "@/common/constants"; @@ -68,22 +71,22 @@ export default function UserForm({ ? z.string().optional() : z .string() - .min(4, t("username_min_length_validation")) - .max(16, t("username_max_length_validation")) - .regex(/^[a-z0-9._-]*$/, t("username_characters_validation")) - .regex(/^[a-z0-9].*[a-z0-9]$/, t("username_start_end_validation")) + .min(4, t("field_required")) + .max(16, t("username_not_valid")) + .regex(/^[a-z0-9._-]*$/, t("username_not_valid")) + .regex(/^[a-z0-9].*[a-z0-9]$/, t("username_not_valid")) .refine( (val) => !val.match(/(?:[._-]{2,})/), - t("username_consecutive_validation"), + t("username_not_valid"), ), password: isEditMode ? z.string().optional() : z .string() - .min(8, t("password_length_validation")) - .regex(/[a-z]/, t("password_lowercase_validation")) - .regex(/[A-Z]/, t("password_uppercase_validation")) - .regex(/[0-9]/, t("password_number_validation")), + .min(8, t("field_required")) + .regex(/[a-z]/, t("new_password_validation")) + .regex(/[A-Z]/, t("new_password_validation")) + .regex(/[0-9]/, t("new_password_validation")), c_password: isEditMode ? z.string().optional() : z.string(), first_name: z.string().min(1, t("field_required")), last_name: z.string().min(1, t("field_required")), @@ -120,6 +123,12 @@ export default function UserForm({ resolver: zodResolver(userFormSchema), defaultValues: { user_type: "staff", + username: "", + password: "", + c_password: "", + first_name: "", + last_name: "", + email: "", phone_number: "", alt_phone_number: "", phone_number_is_whatsapp: true, @@ -133,7 +142,6 @@ export default function UserForm({ }), enabled: !!existingUsername, }); - useEffect(() => { if (userData && isEditMode) { const formData: Partial = { @@ -149,6 +157,9 @@ export default function UserForm({ } }, [userData, form, isEditMode]); + 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"); @@ -179,12 +190,7 @@ export default function UserForm({ const isInitialRender = usernameInput === ""; if (username?.message) { - return validateRule( - false, - username.message, - isInitialRender, - t("username_valid"), - ); + return null; } else if (isUsernameChecking) { return (
@@ -363,10 +369,57 @@ export default function UserForm({ data-cy="username-input" placeholder={t("username")} {...field} + onFocus={() => setIsUsernameFieldFocused(true)} + onBlur={() => setIsUsernameFieldFocused(false)} />
- {renderUsernameFeedback(usernameInput ?? "")} + {isUsernameFieldFocused ? ( + <> +
+ = 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 || "", + ), + }, + ]} + /> +
+
+ {renderUsernameFeedback(usernameInput || "")} +
+ + ) : ( + + )} )} /> @@ -383,9 +436,41 @@ export default function UserForm({ data-cy="password-input" placeholder={t("password")} {...field} + onFocus={() => setIsPasswordFieldFocused(true)} + onBlur={() => setIsPasswordFieldFocused(false)} /> - + {isPasswordFieldFocused ? ( +
+ = 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 || ""), + }, + ]} + /> +
+ ) : ( + + )} )} /> @@ -616,6 +701,7 @@ export default function UserForm({ type="submit" className="w-full" data-cy="submit-user-form" + variant="primary" disabled={ isLoadingUser || !form.formState.isDirty || diff --git a/src/components/Users/UserFormValidations.tsx b/src/components/Users/UserFormValidations.tsx index 4899a6f3d25..f07e08a9718 100644 --- a/src/components/Users/UserFormValidations.tsx +++ b/src/components/Users/UserFormValidations.tsx @@ -1,3 +1,5 @@ +import { Trans } from "react-i18next"; + import CareIcon from "@/CAREUI/icons/CareIcon"; import { classNames } from "@/Utils/utils"; @@ -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 ( +
+ {isInputEmpty && + validations.map((validation, index) => ( +
+ +
+ ))} + {!isInputEmpty && + !allValid && + unfulfilledValidations.map((validation, index) => ( +
+ +
+ ))} + {allValid && ( + <> + + + {successMessage} + + + )} +
+ ); +}; + export const validateRule = ( isConditionMet: boolean, initialMessage: JSX.Element | string, diff --git a/src/components/Users/UserResetPassword.tsx b/src/components/Users/UserResetPassword.tsx index 6b0d343b0dc..fed51b5d5b3 100644 --- a/src/components/Users/UserResetPassword.tsx +++ b/src/components/Users/UserResetPassword.tsx @@ -19,7 +19,7 @@ import { } from "@/components/ui/form"; import { PasswordInput } from "@/components/ui/input-password"; -import { validateRule } from "@/components/Users/UserFormValidations"; +import { ValidationHelper } from "@/components/Users/UserFormValidations"; import { UpdatePasswordForm } from "@/components/Users/models"; import routes from "@/Utils/request/api"; @@ -152,36 +152,33 @@ export default function UserResetPassword({ className="text-small mt-2 pl-2 text-secondary-500" aria-live="polite" > - {validateRule( - field.value.length >= 8, - t("password_length_validation"), - !field.value, - t("password_length_met"), - )} - {validateRule( - /[a-z]/.test(field.value), - t("password_lowercase_validation"), - !field.value, - t("password_lowercase_met"), - )} - {validateRule( - /[A-Z]/.test(field.value), - t("password_uppercase_validation"), - !field.value, - t("password_uppercase_met"), - )} - {validateRule( - /\d/.test(field.value), - t("password_number_validation"), - !field.value, - t("password_number_met"), - )} - {validateRule( - field.value !== form.watch("old_password"), - t("new_password_same_as_old"), - !field.value, - t("new_password_different_from_old"), - )} + = 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), + }, + { + description: "new_password_same_as_old", + fulfilled: + field.value !== form.watch("old_password"), + }, + ]} + />
) : ( diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 1a71cc04227..8a61c10c92a 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -1,4 +1,5 @@ import { Slot } from "@radix-ui/react-slot"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { VariantProps, cva } from "class-variance-authority"; import { PanelLeftClose, PanelRightClose } from "lucide-react"; import * as React from "react"; @@ -9,7 +10,7 @@ import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; -import { Sheet, SheetContent } from "@/components/ui/sheet"; +import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; import { Skeleton } from "@/components/ui/skeleton"; import { Tooltip, @@ -207,6 +208,9 @@ const Sidebar = React.forwardRef< } side={side} > + + Sidebar +
{children}