From 1cc674e69bc7ae5d2927d51b6294810c20d31667 Mon Sep 17 00:00:00 2001 From: Rishav Jha <76212518+rishav-jha-mech@users.noreply.github.com> Date: Tue, 31 Oct 2023 22:04:56 +0530 Subject: [PATCH 1/2] Merge latest AdminUI Redesign into Master (#1006) * Pagination Done for Orglist * Fixed warnings * Infinite scroll and search working for Requests screen * Simplified the code * Infinite scroll enabled and functioning on OrgList Requests and Users screen * FIxed warning * Fixed typo * Fixed bug * Joined and Blockedbyorgs screen and mdoal ready * Tables ready ! * Remove user from organization functionality working well * Update user role in organization feature ready * Minor changes * Done with tests on OrgList * Done with testss of Requests screen * 100% CC achieved for Users screen * Main tests done for UserTableItem * 100% Code Coverage Achieved for UserTableItem * Removed Redundant Landing Page * 100% CC achieved for TableLoader * Translation added for Users Screen * Translation done for Requests screen * Translation done for dashboard screen * Linting and warnings fixed * Improved login page * UI Done for Forgot Password Screen * Forgot Password Screen Tests done! * Fixed all pending tests * Better message for btns and coloring * Linting issues fixed * Fixed code styles * SUPPRESSED UNKNOWN ERROR * Fixed formatting * Updated typoed message * Fixed failing tests accompanying typo --- package-lock.json | 88 +- package.json | 1 + public/locales/en.json | 27 +- public/locales/fr.json | 26 +- public/locales/hi.json | 26 +- public/locales/sp.json | 26 +- public/locales/zh.json | 26 +- src/GraphQl/Mutations/mutations.ts | 17 + src/GraphQl/Queries/Queries.ts | 75 +- src/assets/svgs/key.svg | 4 + src/components/DeleteOrg/DeleteOrg.tsx | 8 +- .../LandingPage/LandingPage.module.css | 26 - .../LandingPage/LandingPage.test.tsx | 44 - src/components/LandingPage/LandingPage.tsx | 23 - src/components/LeftDrawer/LeftDrawer.tsx | 2 +- .../LeftDrawerOrg/LeftDrawerOrg.tsx | 2 +- .../TableLoader/TableLoader.test.tsx | 56 +- src/components/TableLoader/TableLoader.tsx | 53 +- .../UsersTableItem/UserTableItem.test.tsx | 1054 +++++++++++++++++ .../UsersTableItem/UserTableItemMocks.ts | 59 + .../UsersTableItem/UsersTableItem.module.css | 26 + .../UsersTableItem/UsersTableItem.tsx | 598 ++++++++++ .../ForgotPassword/ForgotPassword.module.css | 71 ++ .../ForgotPassword/ForgotPassword.test.tsx | 50 +- src/screens/ForgotPassword/ForgotPassword.tsx | 265 ++--- src/screens/LoginPage/LoginPage.module.css | 55 +- src/screens/LoginPage/LoginPage.tsx | 6 +- src/screens/OrgList/OrgList.module.css | 7 + src/screens/OrgList/OrgList.tsx | 204 +++- src/screens/OrgList/OrgListMocks.ts | 24 +- src/screens/OrgSettings/OrgSettings.test.tsx | 2 +- src/screens/OrgSettings/OrgSettings.tsx | 4 +- .../OrganizationDashboard.test.tsx | 13 +- .../OrganizationDashboard.tsx | 22 +- .../OrganizationPeople.test.tsx | 4 + src/screens/Requests/Requests.test.tsx | 236 +--- src/screens/Requests/Requests.tsx | 260 ++-- src/screens/Requests/RequestsMocks.ts | 220 ++++ src/screens/Users/Users.test.tsx | 326 +---- src/screens/Users/Users.tsx | 255 ++-- src/screens/Users/UsersMocks.ts | 216 ++++ src/utils/interfaces.ts | 51 + 42 files changed, 3424 insertions(+), 1134 deletions(-) create mode 100644 src/assets/svgs/key.svg delete mode 100644 src/components/LandingPage/LandingPage.module.css delete mode 100644 src/components/LandingPage/LandingPage.test.tsx delete mode 100644 src/components/LandingPage/LandingPage.tsx create mode 100644 src/components/UsersTableItem/UserTableItem.test.tsx create mode 100644 src/components/UsersTableItem/UserTableItemMocks.ts create mode 100644 src/components/UsersTableItem/UsersTableItem.module.css create mode 100644 src/components/UsersTableItem/UsersTableItem.tsx create mode 100644 src/screens/Requests/RequestsMocks.ts create mode 100644 src/screens/Users/UsersMocks.ts diff --git a/package-lock.json b/package-lock.json index c43fb82c4f..f11ca8eb3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.8.3", "@mui/material": "^5.14.1", + "@mui/private-theming": "^5.14.13", + "@mui/system": "^5.14.12", "@mui/x-charts": "^6.0.0-alpha.13", "@mui/x-data-grid": "^6.8.0", "@mui/x-date-pickers": "^6.6.0", @@ -57,6 +59,7 @@ "react-dom": "^17.0.2", "react-google-recaptcha": "^2.1.0", "react-i18next": "^11.18.1", + "react-infinite-scroll-component": "^6.1.0", "react-redux": "^7.2.5", "react-router-dom": "^5.2.0", "react-scripts": "5.0.1", @@ -2190,9 +2193,9 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", - "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3470,12 +3473,12 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.14.11", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.11.tgz", - "integrity": "sha512-MSnNNzTu9pfKLCKs1ZAKwOTgE4bz+fQA0fNr8Jm7NDmuWmw0CaN9Vq2/MHsatE7+S0A25IAKby46Uv1u53rKVQ==", + "version": "5.14.15", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.15.tgz", + "integrity": "sha512-V2Xh+Tu6A07NoSpup0P9m29GwvNMYl5DegsGWqlOTJyAV7cuuVjmVPqxgvL8xBng4R85xqIQJRMjtYYktoPNuQ==", "dependencies": { - "@babel/runtime": "^7.22.15", - "@mui/utils": "^5.14.11", + "@babel/runtime": "^7.23.2", + "@mui/utils": "^5.14.15", "prop-types": "^15.8.1" }, "engines": { @@ -3496,11 +3499,11 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.14.11", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.11.tgz", - "integrity": "sha512-jdUlqRgTYQ8RMtPX4MbRZqar6W2OiIb6J5KEFbIu4FqvPrk44Each4ppg/LAqp1qNlBYq5i+7Q10MYLMpDxX9A==", + "version": "5.14.15", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.15.tgz", + "integrity": "sha512-mbOjRf867BysNpexe5Z/P8s3bWzDPNowmKhi7gtNDP/LPEeqAfiDSuC4WPTXmtvse1dCl30Nl755OLUYuoi7Mw==", "dependencies": { - "@babel/runtime": "^7.22.15", + "@babel/runtime": "^7.23.2", "@emotion/cache": "^11.11.0", "csstype": "^3.1.2", "prop-types": "^15.8.1" @@ -3527,15 +3530,15 @@ } }, "node_modules/@mui/system": { - "version": "5.14.11", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.11.tgz", - "integrity": "sha512-yl8xV+y0k7j6dzBsHabKwoShmjqLa8kTxrhUI3JpqLG358VRVMJRW/ES0HhvfcCi4IVXde+Tc2P3K1akGL8zoA==", - "dependencies": { - "@babel/runtime": "^7.22.15", - "@mui/private-theming": "^5.14.11", - "@mui/styled-engine": "^5.14.11", - "@mui/types": "^7.2.4", - "@mui/utils": "^5.14.11", + "version": "5.14.15", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.15.tgz", + "integrity": "sha512-zr0Gdk1RgKiEk+tCMB900LaOpEC8NaGvxtkmMdL/CXgkqQZSVZOt2PQsxJWaw7kE4YVkIe4VukFVc43qcq9u3w==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@mui/private-theming": "^5.14.15", + "@mui/styled-engine": "^5.14.15", + "@mui/types": "^7.2.7", + "@mui/utils": "^5.14.15", "clsx": "^2.0.0", "csstype": "^3.1.2", "prop-types": "^15.8.1" @@ -3574,11 +3577,11 @@ } }, "node_modules/@mui/types": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz", - "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==", + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.7.tgz", + "integrity": "sha512-sofpWmcBqOlTzRbr1cLQuUDKaUYVZTw8ENQrtL39TECRNENEzwgnNPh6WMfqMZlMvf1Aj9DLg74XPjnLr0izUQ==", "peerDependencies": { - "@types/react": "*" + "@types/react": "^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3587,12 +3590,12 @@ } }, "node_modules/@mui/utils": { - "version": "5.14.11", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.11.tgz", - "integrity": "sha512-fmkIiCPKyDssYrJ5qk+dime1nlO3dmWfCtaPY/uVBqCRMBZ11JhddB9m8sjI2mgqQQwRJG5bq3biaosNdU/s4Q==", + "version": "5.14.15", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.15.tgz", + "integrity": "sha512-QBfHovAvTa0J1jXuYDaXGk+Yyp7+Fm8GSqx6nK2JbezGqzCFfirNdop/+bL9Flh/OQ/64PeXcW4HGDdOge+n3A==", "dependencies": { - "@babel/runtime": "^7.22.15", - "@types/prop-types": "^15.7.5", + "@babel/runtime": "^7.23.2", + "@types/prop-types": "^15.7.8", "prop-types": "^15.8.1", "react-is": "^18.2.0" }, @@ -5081,9 +5084,9 @@ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==" }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "version": "15.7.9", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", + "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" }, "node_modules/@types/q": { "version": "1.5.5", @@ -20018,6 +20021,17 @@ } } }, + "node_modules/react-infinite-scroll-component": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", + "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", + "dependencies": { + "throttle-debounce": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -23386,6 +23400,14 @@ "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==" }, + "node_modules/throttle-debounce": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", + "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", diff --git a/package.json b/package.json index cc1e769e3a..1fbc68abf3 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "react-dom": "^17.0.2", "react-google-recaptcha": "^2.1.0", "react-i18next": "^11.18.1", + "react-infinite-scroll-component": "^6.1.0", "react-redux": "^7.2.5", "react-router-dom": "^5.2.0", "react-scripts": "5.0.1", diff --git a/public/locales/en.json b/public/locales/en.json index 37d239a2e8..57f7b0d07b 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -86,7 +86,7 @@ "cancel": "Cancel", "noOrgErrorTitle": "Organizations Not Found", "noOrgErrorDescription": "Please create an organization through dashboard", - + "endOfResults": "End of results", "manageFeatures": "Manage Features", "manageFeaturesInfo": "Creation Successful ! Please select features that you want to enale for this organization from the plugin store.", "goToStore": "Go to Plugin Store", @@ -109,6 +109,15 @@ "name": "Name", "email": "Email", "roles_userType": "Role/User-Type", + "joined_organizations": "Joined Organizations", + "blocked_organizations": "Blocked Organizations", + "orgJoinedBy": "Organizations Joined By", + "orgThatBlocked": "Organizations That Blocked", + "endOfResults": "End of results", + "hasNotJoinedAnyOrg": "has not joined any organization", + "isNotBlockedByAnyOrg": "is not blocked by any organization", + "searchByOrgName": "Search By Organization Name", + "view": "View", "admin": "ADMIN", "superAdmin": "SUPERADMIN", "user": "USER", @@ -131,6 +140,7 @@ "accept": "Accept", "reject": "Reject", "enterName": "Enter Name", + "endOfResults": "End of results", "loadingRequests": "Loading Requests...", "noRequestFound": "No Request Found", "sort": "Sort", @@ -153,6 +163,13 @@ "events": "Events", "blockedUsers": "Blocked Users", "requests": "Requests", + "viewAll": "View All", + "upcomingEvents": "Upcoming Events", + "noUpcomingEvents": "No Upcoming Events", + "latestPosts": "Latest Posts", + "noPostsPresent": "No Posts Present", + "membershipRequests": "Membership requests", + "noMembershipRequests": "No Membership requests present", "talawaApiUnavailable": "Talawa-API service is unavailable. Is it running? Check your network connectivity too." }, "organizationPeople": { @@ -328,7 +345,7 @@ "enterNewPassword": "Enter New Password", "cofirmNewPassword": "Confirm New Password", "changePassword": "Change Password", - "backToHome": "Back to Home", + "backToLogin": "Back to Login", "userOtp": "e.g. 12345", "password": "Password", "emailNotRegistered": "Email is not registered.", @@ -377,9 +394,9 @@ "deleteOrg": { "deleteOrganization": "Delete Organization", "deleteMsg": "Do you want to delete this organization?", - "no": "No", - "yes": "Yes", - "longDelOrgMsg": "By clicking on Delete organization button you will the organization will be permanently deleted along with its events, tags and all related data." + "cancel": "Cancel", + "confirmDelete": "Confirm Delete", + "longDelOrgMsg": "By clicking on Delete Organization button the organization will be permanently deleted along with its events, tags and all related data." }, "userUpdate": { "firstName": "First Name", diff --git a/public/locales/fr.json b/public/locales/fr.json index 0d483a607f..7e148bb4b4 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -84,6 +84,7 @@ "sort": "Trier", "filter": "Filtre", "cancel": "Annuler", + "endOfResults": "Fin des résultats", "noOrgErrorTitle": "Organisations non trouvées", "noOrgErrorDescription": "Veuillez créer une organisation via le tableau de bord", "noResultsFoundFor": "Aucun résultat trouvé pour " @@ -104,6 +105,15 @@ "name": "Nom", "email": "E-mail", "roles_userType": "Rôle/Type d'utilisateur", + "joined_organizations": "Organisations rejointes", + "blocked_organizations": "Organisations bloquées", + "endOfResults": "Fin des résultats", + "orgJoinedBy": "Organisation rejointe par", + "orgThatBlocked": "Organisation bloquée par", + "hasNotJoinedAnyOrg": "n'a rejoint aucune organisation", + "isNotBlockedByAnyOrg": "n'est bloqué par aucune organisation", + "searchByOrgName": "Rechercher par nom d'organisation", + "view": "Vue", "admin": "ADMIN", "superAdmin": "SUPERADMIN", "user": "UTILISATEUR", @@ -125,6 +135,7 @@ "accept": "Accepter", "reject": "Rejeter", "enterName": "Entrez le nom", + "endOfResults": "Fin des résultats", "loadingRequests": "Chargement des demandes...", "noRequestFound": "Aucune demande trouvée", "sort": "Trier", @@ -146,6 +157,13 @@ "events": "Événements", "blockedUsers": "Utilisateurs bloqués", "requests": "Demandes", + "viewAll": "Voir tout", + "upcomingEvents": "Événements à venir", + "noUpcomingEvents": "Aucun événement à venir", + "latestPosts": "Derniers messages", + "noPostsPresent": "Aucune publication présente", + "membershipRequests": "Demandes d'adhésion", + "noMembershipRequests": "Aucune demande d'adhésion", "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Est-il en cours d'exécution ? Vérifiez également votre connectivité réseau." }, "organizationPeople": { @@ -321,7 +339,7 @@ "enterNewPassword": "Entrez un nouveau mot de passe", "cofirmNewPassword": "Confirmer le nouveau mot de passe", "changePassword": "Changer le mot de passe", - "backToHome": "De retour à la maison", + "backToLogin": "Retour à la connexion", "userOtp": "par exemple. 12345", "password": "Mot de passe", "emailNotRegistered": "L'e-mail n'est pas enregistré.", @@ -372,9 +390,9 @@ "deleteOrg": { "deleteOrganization": "Supprimer l'organisation", "deleteMsg": "Voulez-vous supprimer cette organisation ?", - "no": "Non", - "yes": "Oui", - "longDelOrgMsg": "En cliquant sur le bouton Supprimer l'organisation, l'organisation sera définitivement supprimée, ainsi que ses événements, étiquettes et toutes les données associées." + "cancel": "Annuler", + "confirmDelete": "Confirmer la suppression", + "longDelOrgMsg": "En cliquant sur le bouton Supprimer l'organisation, l'organisation sera définitivement supprimée ainsi que ses événements, balises et toutes les données associées." }, "userUpdate": { "firstName": "Prénom", diff --git a/public/locales/hi.json b/public/locales/hi.json index e5fc2584ff..48a97211f8 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -84,6 +84,7 @@ "sort": "छांटें", "filter": "फ़िल्टर", "cancel": "रद्द करना", + "endOfResults": "परिणामों का अंत", "noOrgErrorTitle": "संगठन नहीं मिला", "noOrgErrorDescription": "कृपया डैशबोर्ड के माध्यम से एक संगठन बनाएं", "noResultsFoundFor": "के लिए कोई परिणाम नहीं मिला " @@ -104,6 +105,15 @@ "name": "नाम", "email": "ईमेल", "roles_userType": "भूमिका/उपयोगकर्ता-प्रकार", + "joined_organizations": "संगठनों में शामिल हुए", + "blocked_organizations": "अवरोधित संगठन", + "endOfResults": "परिणामों का अंत", + "orgJoinedBy": "द्वारा शामिल हुए संगठन", + "orgThatBlocked": "जिन संगठनों ने अवरोधित किया", + "hasNotJoinedAnyOrg": "किसी भी संगठन में शामिल नहीं है", + "isNotBlockedByAnyOrg": "किसी भी संगठन द्वारा अवरोधित नहीं है", + "searchByOrgName": "संगठन के नाम से खोजें", + "view": "देखें", "admin": "व्यवस्थापक", "superAdmin": "सुपरएडमिन", "user": "उपयोगकर्ता", @@ -125,6 +135,7 @@ "accept": "स्वीकार करना", "reject": "अस्वीकार", "enterName": "नाम दर्ज करें", + "endOfResults": "परिणामों का अंत", "loadingRequests": "अनुरोध लोड हो रहा है ...", "noRequestFound": "कोई अनुरोध नहीं मिला।", "sort": "छांटें", @@ -146,6 +157,13 @@ "events": "आयोजन", "blockedUsers": "रोके गए उपयोगकर्ता", "requests": "अनुरोध", + "viewAll": "सभी देखें", + "upcomingEvents": "आगामी घटनाएँ", + "noUpcomingEvents": "कोई आगामी घटनाएँ नहीं", + "latestPosts": "नवीनतम पोस्ट", + "noPostsPresent": "कोई पोस्ट नहीं है", + "membershipRequests": "सदस्यता अनुरोध", + "noMembershipRequests": "कोई सदस्यता अनुरोध नहीं", "talawaApiUnavailable": "तलवा-एपीआई सेवा उपलब्ध नहीं है। क्या यह चल रहा है? अपनी नेटवर्क कनेक्टिविटी भी जांचें।" }, "organizationPeople": { @@ -321,7 +339,7 @@ "enterNewPassword": "नया पासवर्ड दर्ज करें", "cofirmNewPassword": "नए पासवर्ड की पुष्टि करें", "changePassword": "पासवर्ड बदलें", - "backToHome": "घर वापिस जा रहा हूँ", + "backToLogin": "लॉगिन पर वापस जाएं", "userOtp": "उदाहरण के लिए 12345", "password": "पासवर्ड", "emailNotRegistered": "ईमेल पंजीकृत नहीं है।", @@ -372,9 +390,9 @@ "deleteOrg": { "deleteOrganization": "संगठन हटाएं", "deleteMsg": "क्या आप इस संगठन को हटाना चाहते हैं?", - "no": "नहीं", - "yes": "हां", - "longDelOrgMsg": "संगठन हटाने के बटन पर क्लिक करके, संगठन को स्थायित रूप से हटा दिया जाएगा, साथ ही उसके आयोजन, टैग और सभी संबंधित डेटा भी हटा दिया जाएगा।" + "cancel": "रद्द करना", + "confirmDelete": "हटाने की पुष्टि करें", + "longDelOrgMsg": "संगठन को हमेशा के लिए हटा देने के लिए संगठन हटाने के बटन पर क्लिक करके, उसके इवेंट्स, टैग्स और सभी संबंधित डेटा सहित सभी जानकारी हटा दी जाएगी।" }, "userUpdate": { "firstName": "पहला नाम", diff --git a/public/locales/sp.json b/public/locales/sp.json index b65d83558d..dfcb6165e5 100644 --- a/public/locales/sp.json +++ b/public/locales/sp.json @@ -84,6 +84,7 @@ "sort": "Ordenar", "filter": "Filtrar", "cancel": "Cancelar", + "endOfResults": "Fin de los resultados", "noOrgErrorTitle": "Organizaciones no encontradas", "noOrgErrorDescription": "Por favor, crea una organización a través del panel de control", "noResultsFoundFor": "No se encontraron resultados para " @@ -104,6 +105,15 @@ "name": "Nombre", "email": "Correo electrónico", "roles_userType": "Rol/Tipo de usuario", + "joined_organizations": "Organizaciones unidas", + "blocked_organizations": "Organizaciones bloqueadas", + "endOfResults": "Fin de los resultados", + "orgJoinedBy": "Organizaciones unidas por", + "orgThatBlocked": "Organizaciones bloqueadas por", + "hasNotJoinedAnyOrg": "No se ha unido a ninguna organización.", + "isNotBlockedByAnyOrg": "No está bloqueado por ninguna organización.", + "searchByOrgName": "Buscar por nombre de organización", + "view": "Ver", "admin": "ADMINISTRACIÓN", "superAdmin": "SUPERADMIN", "user": "USUARIO", @@ -125,6 +135,7 @@ "accept": "Aceptar", "reject": "Rechazar", "enterName": "Ingrese su nombre", + "endOfResults": "Fin de los resultados", "loadingRequests": "Cargando solicitudes ...", "noRequestFound": "No se encontró ninguna solicitud.", "sort": "Ordenar", @@ -146,6 +157,13 @@ "events": "Eventos", "blockedUsers": "Usuarios bloqueados", "requests": "Solicitudes", + "viewAll": "Ver Todo", + "upcomingEvents": "Próximos Eventos", + "noUpcomingEvents": "No Hay Próximos Eventos", + "latestPosts": "Últimas Publicaciones", + "noPostsPresent": "No Hay Publicaciones Presentes", + "membershipRequests": "Solicitudes de Membresía", + "noMembershipRequests": "No Hay Solicitudes de Membresía", "talawaApiUnavailable": "El servicio Talawa-API no está disponible. ¿Está funcionando? Compruebe también la conectividad de su red." }, "organizationPeople": { @@ -321,7 +339,7 @@ "enterNewPassword": "Ingrese nueva clave", "cofirmNewPassword": "Confirmar nueva contraseña", "changePassword": "Cambia la contraseña", - "backToHome": "De vuelta a casa", + "backToLogin": "Volver al inicio de sesión", "userOtp": "por ejemplo 12345", "password": "Contraseña", "emailNotRegistered": "El correo electrónico no está registrado.", @@ -372,9 +390,9 @@ "deleteOrg": { "deleteOrganization": "Eliminar organización", "deleteMsg": "¿Desea eliminar esta organización?", - "no": "No", - "yes": "Sí", - "longDelOrgMsg": "Al hacer clic en el botón de Eliminar organización, se eliminará permanentemente la organización junto con sus eventos, etiquetas y todos los datos relacionados." + "cancel": "Cancelar", + "confirmDelete": "Confirmar eliminación", + "longDelOrgMsg": "Al hacer clic en el botón Eliminar organización, la organización se eliminará permanentemente junto con sus eventos, etiquetas y todos los datos relacionados." }, "userUpdate": { "firstName": "Primer nombre", diff --git a/public/locales/zh.json b/public/locales/zh.json index 1a95a15dd0..d55a606326 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -84,6 +84,7 @@ "sort": "排序", "filter": "過濾", "cancel": "取消", + "endOfResults": "結果結束", "noOrgErrorTitle": "找不到组织", "noOrgErrorDescription": "请通过仪表板创建一个组织", "noResultsFoundFor": "未找到结果 " @@ -104,6 +105,15 @@ "name": "姓名", "email": "電子郵件", "roles_userType": "角色/用戶類型", + "joined_organizations": "加入的組織", + "blocked_organizations": "阻止的組織", + "endOfResults": "結果結束", + "orgJoinedBy": "加入組織", + "orgThatBlocked": "阻止組織", + "hasNotJoinedAnyOrg": "尚未加入任何組織", + "isNotBlockedByAnyOrg": "沒有被任何組織阻止", + "searchByOrgName": "按組織名稱搜索", + "view": "視圖", "admin": "行政", "superAdmin": "超級管理員", "user": "用戶", @@ -125,6 +135,7 @@ "accept": "接受", "reject": "拒絕", "enterName": "输入名字", + "endOfResults": "結果結束", "loadingRequests": "正在加載請求...", "noRequestFound": "找不到請求。", "sort": "排序", @@ -146,6 +157,13 @@ "events": "事件", "blockedUsers": "被阻止的用戶", "requests": "请求", + "viewAll": "查看全部", + "upcomingEvents": "即将举办的活动", + "noUpcomingEvents": "没有即将举办的活动", + "latestPosts": "最新帖子", + "noPostsPresent": "没有帖子", + "membershipRequests": "会员申请", + "noMembershipRequests": "没有会员申请", "talawaApiUnavailable": "服務不可用。它在運行嗎?還要檢查您的網絡連接。" }, "organizationPeople": { @@ -321,7 +339,7 @@ "enterNewPassword": "輸入新密碼", "cofirmNewPassword": "確認新密碼", "changePassword": "更改密碼", - "backToHome": "回到家", + "backToLogin": "回到登錄", "userOtp": "舉個例子。 12345", "password": "密碼", "emailNotRegistered": "電子郵件未註冊。", @@ -372,9 +390,9 @@ "deleteOrg": { "deleteOrganization": "删除组织", "deleteMsg": "您是否要删除此组织?", - "no": "否", - "yes": "是", - "longDelOrgMsg": "点击删除组织按钮后,将永久删除该组织以及其活动、标签和所有相关数据。" + "cancel": "取消", + "confirmDelete": "确认删除", + "longDelOrgMsg": "点击删除组织按钮,组织将被永久删除,包括其事件、标签和所有相关数据。" }, "userUpdate": { "firstName": "名", diff --git a/src/GraphQl/Mutations/mutations.ts b/src/GraphQl/Mutations/mutations.ts index 9444bdcd48..7090bd10d7 100644 --- a/src/GraphQl/Mutations/mutations.ts +++ b/src/GraphQl/Mutations/mutations.ts @@ -623,6 +623,23 @@ export const UNLIKE_COMMENT = gql` } `; +// Changes the role of a user in an organization +export const UPDATE_USER_ROLE_IN_ORG_MUTATION = gql` + mutation updateUserRoleInOrganization( + $organizationId: ID! + $userId: ID! + $role: String! + ) { + updateUserRoleInOrganization( + organizationId: $organizationId + userId: $userId + role: $role + ) { + _id + } + } +`; + export const CREATE_DIRECT_CHAT = gql` mutation createDirectChat($userIds: [ID!]!, $organizationId: ID!) { createDirectChat( diff --git a/src/GraphQl/Queries/Queries.ts b/src/GraphQl/Queries/Queries.ts index 95dfff3eb2..13dfc9c135 100644 --- a/src/GraphQl/Queries/Queries.ts +++ b/src/GraphQl/Queries/Queries.ts @@ -42,8 +42,12 @@ export const ORGANIZATION_LIST = gql` // Query to take the Organization list with filter option export const ORGANIZATION_CONNECTION_LIST = gql` - query OrganizationsConnection($filter: String) { - organizationsConnection(where: { name_contains: $filter }) { + query OrganizationsConnection($filter: String, $first: Int, $skip: Int) { + organizationsConnection( + where: { name_contains: $filter } + first: $first + skip: $skip + ) { _id image creator { @@ -64,14 +68,20 @@ export const ORGANIZATION_CONNECTION_LIST = gql` `; // Query to take the User list - export const USER_LIST = gql` - query Users($firstName_contains: String, $lastName_contains: String) { + query Users( + $firstName_contains: String + $lastName_contains: String + $skip: Int + $first: Int + ) { users( where: { firstName_contains: $firstName_contains lastName_contains: $lastName_contains } + skip: $skip + first: $first ) { firstName lastName @@ -80,13 +90,70 @@ export const USER_LIST = gql` email userType adminApproved + adminFor { + _id + } + createdAt organizationsBlockedBy { _id name + image + location + createdAt + creator { + _id + firstName + lastName + image + email + createdAt + } } joinedOrganizations { _id + name + image + location + createdAt + creator { + _id + firstName + lastName + image + email + createdAt + } + } + } + } +`; + +export const USER_LIST_REQUEST = gql` + query Users( + $firstName_contains: String + $lastName_contains: String + $first: Int + $skip: Int + $userType: String + $adminApproved: Boolean + ) { + users( + where: { + firstName_contains: $firstName_contains + lastName_contains: $lastName_contains } + skip: $skip + first: $first + userType: $userType + adminApproved: $adminApproved + ) { + firstName + lastName + image + _id + email + userType + adminApproved createdAt } } diff --git a/src/assets/svgs/key.svg b/src/assets/svgs/key.svg new file mode 100644 index 0000000000..a1f47615e8 --- /dev/null +++ b/src/assets/svgs/key.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/DeleteOrg/DeleteOrg.tsx b/src/components/DeleteOrg/DeleteOrg.tsx index e6442d6558..91fe4263f4 100644 --- a/src/components/DeleteOrg/DeleteOrg.tsx +++ b/src/components/DeleteOrg/DeleteOrg.tsx @@ -66,18 +66,18 @@ function deleteOrg(): JSX.Element { {t('deleteMsg')} diff --git a/src/components/LandingPage/LandingPage.module.css b/src/components/LandingPage/LandingPage.module.css deleted file mode 100644 index c912b5f41d..0000000000 --- a/src/components/LandingPage/LandingPage.module.css +++ /dev/null @@ -1,26 +0,0 @@ -.maintitle { - font-size: 18px; - color: #707070; - width: 70%; - margin: 35px auto 10px; - text-align: center; -} -.maintitle > span { - color: black; -} -.fromtitle { - font-size: 14px; - color: #707070; - margin: 40px auto 10px; - text-align: center; -} -.carouseldiv { - z-index: 1; - margin: 35px auto 10px; - align-items: center; - width: 80%; -} - -.carouseldiv img { - border-radius: 5px; -} diff --git a/src/components/LandingPage/LandingPage.test.tsx b/src/components/LandingPage/LandingPage.test.tsx deleted file mode 100644 index b4e798bab7..0000000000 --- a/src/components/LandingPage/LandingPage.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { Suspense } from 'react'; -import { render, waitFor } from '@testing-library/react'; -import { I18nextProvider } from 'react-i18next'; - -import LandingPage from './LandingPage'; -import type { NormalizedCacheObject } from '@apollo/client'; -import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'; -import i18n from 'utils/i18n'; -import { BACKEND_URL } from 'Constant/constant'; - -const client: ApolloClient = new ApolloClient({ - cache: new InMemoryCache(), - uri: BACKEND_URL, -}); - -describe('Testing LandingPage', () => { - const fallbackLoader =
; - - test('should render props and text elements test for the page component', async () => { - render( - - - - - - - - ); - - await waitFor(() => { - const elements = document.querySelectorAll('*'); - const searchText = 'loginPage.fromPalisadoes'; - - for (let i = 0; i < elements.length; i++) { - const element = elements[i] as HTMLElement; - const text = element.innerText; - - if (text && text.includes(searchText)) { - break; - } - } - }); - }); -}); diff --git a/src/components/LandingPage/LandingPage.tsx b/src/components/LandingPage/LandingPage.tsx deleted file mode 100644 index fadb028dc9..0000000000 --- a/src/components/LandingPage/LandingPage.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import styles from './LandingPage.module.css'; -import slide1 from 'assets/images/palisadoes_logo.png'; -import { useTranslation } from 'react-i18next'; - -function landingPage(): JSX.Element { - const { t } = useTranslation('translation', { keyPrefix: 'loginPage' }); - - return ( - <> -
-
- First slide -
-

-

{t('fromPalisadoes')}

-

-
- - ); -} -export {}; -export default landingPage; diff --git a/src/components/LeftDrawer/LeftDrawer.tsx b/src/components/LeftDrawer/LeftDrawer.tsx index b5244b2df7..57eacce0a7 100644 --- a/src/components/LeftDrawer/LeftDrawer.tsx +++ b/src/components/LeftDrawer/LeftDrawer.tsx @@ -139,7 +139,7 @@ const leftDrawer = ({ }} >
- {userImage && userImage ? ( + {userImage && userImage !== 'null' ? ( {`profile ) : (
- {userImage && userImage ? ( + {userImage && userImage !== 'null' ? ( {`profile ) : ( { + console.error = jest.fn(); +}); describe('Testing Loader component', () => { - test('Component should be rendered properly', () => { + test('Component should be rendered properly only headerTitles is provided', () => { + const props: InterfaceTableLoader = { + noOfRows: 10, + headerTitles: ['header1', 'header2', 'header3'], + }; render( ); // Check if header titles are rendered properly - props.headerTitles.forEach((title) => { + const data = props.headerTitles as string[]; + data.forEach((title) => { expect(screen.getByText(title)).toBeInTheDocument(); }); @@ -26,11 +31,48 @@ describe('Testing Loader component', () => { expect( screen.getByTestId(`row-${rowIndex}-tableLoading`) ).toBeInTheDocument(); - for (let colIndex = 0; colIndex < props.headerTitles.length; colIndex++) { + for (let colIndex = 0; colIndex < data.length; colIndex++) { + expect( + screen.getByTestId(`row-${rowIndex}-col-${colIndex}-tableLoading`) + ).toBeInTheDocument(); + } + } + }); + test('Component should be rendered properly only noCols is provided', () => { + const props: InterfaceTableLoader = { + noOfRows: 10, + noOfCols: 3, + }; + render( + + + + ); + // Check if header titles are rendered properly + const data = [...Array(props.noOfCols)]; + + // Check if elements are rendered properly + for (let rowIndex = 0; rowIndex < props.noOfRows; rowIndex++) { + expect( + screen.getByTestId(`row-${rowIndex}-tableLoading`) + ).toBeInTheDocument(); + for (let colIndex = 0; colIndex < data.length; colIndex++) { expect( screen.getByTestId(`row-${rowIndex}-col-${colIndex}-tableLoading`) ).toBeInTheDocument(); } } }); + test('Component should be throw error when noOfCols and headerTitles are undefined', () => { + const props = { + noOfRows: 10, + }; + expect(() => { + render( + + + + ); + }).toThrowError(); + }); }); diff --git a/src/components/TableLoader/TableLoader.tsx b/src/components/TableLoader/TableLoader.tsx index 49d317b292..2da4b1d172 100644 --- a/src/components/TableLoader/TableLoader.tsx +++ b/src/components/TableLoader/TableLoader.tsx @@ -1,25 +1,40 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import styles from './TableLoader.module.css'; import { Table } from 'react-bootstrap'; -interface InterfaceTableLoader { +export interface InterfaceTableLoader { noOfRows: number; - headerTitles: string[]; + headerTitles?: string[]; + noOfCols?: number; } const tableLoader = (props: InterfaceTableLoader): JSX.Element => { - const { noOfRows, headerTitles } = props; + const { noOfRows, headerTitles, noOfCols } = props; + + useEffect(() => { + if (headerTitles == undefined && noOfCols == undefined) { + throw new Error( + 'TableLoader error Either headerTitles or noOfCols is required !' + ); + } + }, []); return ( <> - +
- {headerTitles.map((title, index) => { - return ; - })} + {headerTitles + ? headerTitles.map((title, index) => { + return ; + }) + : noOfCols && + [...Array(noOfCols)].map((_, index) => { + return + {[...Array(noOfRows)].map((_, rowIndex) => { return ( @@ -28,16 +43,18 @@ const tableLoader = (props: InterfaceTableLoader): JSX.Element => { className="mb-4" data-testid={`row-${rowIndex}-tableLoading`} > - {[...Array(headerTitles?.length)].map((_, colIndex) => { - return ( - - ); - })} + {[...Array(headerTitles ? headerTitles?.length : noOfCols)].map( + (_, colIndex) => { + return ( + + ); + } + )} ); })} diff --git a/src/components/UsersTableItem/UserTableItem.test.tsx b/src/components/UsersTableItem/UserTableItem.test.tsx new file mode 100644 index 0000000000..0e67a02022 --- /dev/null +++ b/src/components/UsersTableItem/UserTableItem.test.tsx @@ -0,0 +1,1054 @@ +import React from 'react'; +import { MockedProvider } from '@apollo/react-testing'; +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { I18nextProvider } from 'react-i18next'; +import { toast } from 'react-toastify'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import i18nForTest from 'utils/i18nForTest'; +import type { InterfaceQueryUserListItem } from 'utils/interfaces'; +import { MOCKS } from './UserTableItemMocks'; +import UsersTableItem from './UsersTableItem'; + +const link = new StaticMockLink(MOCKS, true); + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} +const resetAndRefetchMock = jest.fn(); + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + warning: jest.fn(), + }, +})); + +Object.defineProperty(window, 'location', { + value: { + replace: jest.fn(), + }, + writable: true, +}); + +const mockHistoryPush = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: mockHistoryPush, + }), +})); + +beforeEach(() => { + localStorage.setItem('UserType', 'SUPERADMIN'); + localStorage.setItem('id', '123'); +}); + +afterEach(() => { + localStorage.clear(); + jest.clearAllMocks(); +}); + +describe('Testing User Table Item', () => { + console.error = jest.fn((message) => { + if (message.includes('validateDOMNesting')) { + return; + } + // Log other console errors + console.warn(message); + }); + test('Should render props and text elements test for the page component', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + userType: 'SUPERADMIN', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + ], + createdAt: '2023-09-29T15:39:36.355Z', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'XYZ', + image: null, + location: 'Jamaica', + createdAt: '2023-01-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'mno', + name: 'MNO', + image: null, + location: 'Jamaica', + createdAt: '2023-01-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: null, + location: 'Jamaica', + createdAt: '2023-06-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'def', + name: 'Joined Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-07-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + }, + index: 0, + loggedInUserId: '123', + resetAndRefetch: resetAndRefetchMock, + }; + + render( + + + + + + ); + + await wait(); + expect(screen.getByText(/1/i)).toBeInTheDocument(); + expect(screen.getByText(/John Doe/i)).toBeInTheDocument(); + expect(screen.getByText(/john@example.com/i)).toBeInTheDocument(); + expect(screen.getByTestId(`changeRole${123}`)).toBeInTheDocument(); + expect(screen.getByTestId(`changeRole${123}`)).toHaveValue( + `SUPERADMIN?${123}` + ); + expect(screen.getByTestId(`showJoinedOrgsBtn${123}`)).toBeInTheDocument(); + expect( + screen.getByTestId(`showBlockedByOrgsBtn${123}`) + ).toBeInTheDocument(); + }); + + test('Should render elements correctly when JoinedOrgs and BlockedByOrgs are empty', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + userType: 'SUPERADMIN', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + ], + createdAt: '2023-09-29T15:39:36.355Z', + organizationsBlockedBy: [], + joinedOrganizations: [], + }, + index: 0, + loggedInUserId: '123', + resetAndRefetch: resetAndRefetchMock, + }; + render( + + + + + + ); + + await wait(); + const showJoinedOrgsBtn = screen.getByTestId(`showJoinedOrgsBtn${123}`); // 123 is userId + const showBlockedByOrgsBtn = screen.getByTestId( + `showBlockedByOrgsBtn${123}` + ); // 123 is userId + + // Open JoinedOrgs Modal -> Expect modal to contain text and no search box -> Close Modal + fireEvent.click(showJoinedOrgsBtn); + expect( + screen.queryByTestId(`searchByNameJoinedOrgs`) + ).not.toBeInTheDocument(); + expect( + screen.getByText(/John Doe has not joined any organization/i) + ).toBeInTheDocument(); + fireEvent.click(screen.getByTestId(`closeJoinedOrgsBtn${123}`)); + + // Open BlockedByOrgs Modal -> Expect modal to contain text and no search box -> Close Modal + fireEvent.click(showBlockedByOrgsBtn); + expect( + screen.queryByTestId(`searchByNameOrgsBlockedBy`) + ).not.toBeInTheDocument(); + expect( + screen.getByText(/John Doe is not blocked by any organization/i) + ).toBeInTheDocument(); + fireEvent.click(screen.getByTestId(`closeBlockedByOrgsBtn${123}`)); + }); + + test('Should render props and text elements test for the Joined Organizations Modal properly', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + userType: 'SUPERADMIN', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + ], + createdAt: '2022-09-29T15:39:36.355Z', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'Blocked Organization 1', + image: null, + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'mno', + name: 'Blocked Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=Joined%20Organization%201', + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + }, + }, + { + _id: 'def', + name: 'Joined Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-19T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + }, + index: 0, + loggedInUserId: '123', + resetAndRefetch: resetAndRefetchMock, + }; + + render( + + + + + + ); + + await wait(); + const showJoinedOrgsBtn = screen.getByTestId(`showJoinedOrgsBtn${123}`); + expect(showJoinedOrgsBtn).toBeInTheDocument(); + fireEvent.click(showJoinedOrgsBtn); + expect(screen.getByTestId('modal-joined-org-123')).toBeInTheDocument(); + + // Close using escape key and reopen + fireEvent.keyDown(screen.getByTestId('modal-joined-org-123'), { + key: 'Escape', + code: 'Escape', + keyCode: 27, + charCode: 27, + }); + expect( + screen.queryByRole('dialog')?.className.includes('show') + ).toBeFalsy(); + fireEvent.click(showJoinedOrgsBtn); + // Close using close button and reopen + fireEvent.click(screen.getByTestId(`closeJoinedOrgsBtn${123}`)); + expect( + screen.queryByRole('dialog')?.className.includes('show') + ).toBeFalsy(); + + fireEvent.click(showJoinedOrgsBtn); + + // Expect the following to exist in modal + const inputBox = screen.getByTestId(`searchByNameJoinedOrgs`); + expect(inputBox).toBeInTheDocument(); + expect(screen.getByText(/Joined Organization 1/i)).toBeInTheDocument(); + expect(screen.getByText(/Joined Organization 2/i)).toBeInTheDocument(); + expect(screen.getAllByText(/Jamaica/i)).toHaveLength(2); + expect(screen.getByText(/29-08-2023/i)).toBeInTheDocument(); + expect(screen.getByText(/19-09-2023/i)).toBeInTheDocument(); + expect(screen.getByTestId('removeUserFromOrgBtnabc')).toBeInTheDocument(); + expect(screen.getByTestId('removeUserFromOrgBtndef')).toBeInTheDocument(); + expect(screen.getByTestId(`changeRoleInOrgabc`)).toHaveValue('ADMIN?abc'); + expect(screen.getByTestId(`changeRoleInOrgdef`)).toHaveValue('USER?def'); + + // Search for Joined Organization 1 + fireEvent.change(inputBox, { target: { value: 'Joined Organization 1' } }); + expect(screen.getByText(/Joined Organization 1/i)).toBeInTheDocument(); + expect( + screen.queryByText(/Joined Organization 2/i) + ).not.toBeInTheDocument(); + + // Search for an Organization which does not exist + fireEvent.change(inputBox, { target: { value: 'Joined Organization 3' } }); + expect( + screen.getByText(`No results found for "Joined Organization 3"`) + ).toBeInTheDocument(); + + // Now clear the search box + fireEvent.change(inputBox, { target: { value: '' } }); + + // Click on Creator Link + fireEvent.click(screen.getByTestId(`creatorabc`)); + expect(toast.success).toBeCalledWith('Profile Page Coming Soon !'); + + // Click on Organization Link + fireEvent.click(screen.getByText(/Joined Organization 1/i)); + expect(window.location.replace).toBeCalledWith('/orgdash/id=abc'); + expect(mockHistoryPush).toBeCalledWith('/orgdash/id=abc'); + fireEvent.click(screen.getByTestId(`closeJoinedOrgsBtn${123}`)); + }); + + test('Should render props and text elements test for the Blocked By Organizations Modal properly', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + userType: 'SUPERADMIN', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + { + _id: 'xyz', + }, + ], + createdAt: '2022-09-29T15:39:36.355Z', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'Blocked Organization 1', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=Blocked%20Organization%201', + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + }, + }, + { + _id: 'mno', + name: 'Blocked Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: null, + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'def', + name: 'Joined Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-19T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + }, + index: 0, + loggedInUserId: '123', + resetAndRefetch: resetAndRefetchMock, + }; + + render( + + + + + + ); + + await wait(); + const showBlockedByOrgsBtn = screen.getByTestId( + `showBlockedByOrgsBtn${123}` + ); + expect(showBlockedByOrgsBtn).toBeInTheDocument(); + fireEvent.click(showBlockedByOrgsBtn); + expect(screen.getByTestId('modal-blocked-org-123')).toBeInTheDocument(); + + // Close using escape key and reopen + fireEvent.keyDown(screen.getByTestId('modal-blocked-org-123'), { + key: 'Escape', + code: 'Escape', + keyCode: 27, + charCode: 27, + }); + expect( + screen.queryByRole('dialog')?.className.includes('show') + ).toBeFalsy(); + fireEvent.click(showBlockedByOrgsBtn); + // Close using close button and reopen + fireEvent.click(screen.getByTestId(`closeBlockedByOrgsBtn${123}`)); + expect( + screen.queryByRole('dialog')?.className.includes('show') + ).toBeFalsy(); + + fireEvent.click(showBlockedByOrgsBtn); + + // Expect the following to exist in modal + + const inputBox = screen.getByTestId(`searchByNameOrgsBlockedBy`); + expect(inputBox).toBeInTheDocument(); + expect(screen.getByText(/Blocked Organization 1/i)).toBeInTheDocument(); + expect(screen.getByText(/Blocked Organization 2/i)).toBeInTheDocument(); + expect(screen.getAllByText(/Jamaica/i)).toHaveLength(2); + expect(screen.getByText(/29-08-2023/i)).toBeInTheDocument(); + expect(screen.getByText(/29-09-2023/i)).toBeInTheDocument(); + expect(screen.getByTestId('removeUserFromOrgBtnxyz')).toBeInTheDocument(); + expect(screen.getByTestId('removeUserFromOrgBtnmno')).toBeInTheDocument(); + expect(screen.getByTestId(`changeRoleInOrgxyz`)).toHaveValue('ADMIN?xyz'); + expect(screen.getByTestId(`changeRoleInOrgmno`)).toHaveValue('USER?mno'); + // Click on Creator Link + fireEvent.click(screen.getByTestId(`creatorxyz`)); + expect(toast.success).toBeCalledWith('Profile Page Coming Soon !'); + + // Search for Blocked Organization 1 + fireEvent.change(inputBox, { target: { value: 'Blocked Organization 1' } }); + expect(screen.getByText(/Blocked Organization 1/i)).toBeInTheDocument(); + expect( + screen.queryByText(/Blocked Organization 2/i) + ).not.toBeInTheDocument(); + + // Search for an Organization which does not exist + fireEvent.change(inputBox, { target: { value: 'Blocked Organization 3' } }); + expect( + screen.getByText(`No results found for "Blocked Organization 3"`) + ).toBeInTheDocument(); + + // Now clear the search box + fireEvent.change(inputBox, { target: { value: '' } }); + + // Click on Organization Link + fireEvent.click(screen.getByText(/Blocked Organization 1/i)); + expect(window.location.replace).toBeCalledWith('/orgdash/id=xyz'); + expect(mockHistoryPush).toBeCalledWith('/orgdash/id=xyz'); + fireEvent.click(screen.getByTestId(`closeBlockedByOrgsBtn${123}`)); + }); + + test('Remove user from Organization should function properly in Organizations Joined Modal', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + userType: 'SUPERADMIN', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + { + _id: 'xyz', + }, + ], + createdAt: '2022-09-29T15:39:36.355Z', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'Blocked Organization 1', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=Blocked%20Organization%201', + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + }, + }, + { + _id: 'mno', + name: 'Blocked Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: null, + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'def', + name: 'Joined Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-19T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + }, + index: 0, + loggedInUserId: '123', + resetAndRefetch: resetAndRefetchMock, + }; + + render( + + + + + + ); + + await wait(); + const showJoinedOrgsBtn = screen.getByTestId(`showJoinedOrgsBtn${123}`); + expect(showJoinedOrgsBtn).toBeInTheDocument(); + fireEvent.click(showJoinedOrgsBtn); + expect(screen.getByTestId('modal-joined-org-123')).toBeInTheDocument(); + fireEvent.click(showJoinedOrgsBtn); + fireEvent.click(screen.getByTestId(`removeUserFromOrgBtn${'abc'}`)); + expect(screen.getByTestId('modal-remove-user-123')).toBeInTheDocument(); + + // Close using escape key and reopen + fireEvent.keyDown(screen.getByTestId('modal-joined-org-123'), { + key: 'Escape', + code: 'Escape', + keyCode: 27, + charCode: 27, + }); + expect( + screen + .queryAllByRole('dialog') + .some((el) => el.className.includes('show')) + ).toBeTruthy(); + fireEvent.click(showJoinedOrgsBtn); + // Close using close button and reopen + fireEvent.click(screen.getByTestId('closeRemoveUserModal123')); + expect( + screen + .queryAllByRole('dialog') + .some((el) => el.className.includes('show')) + ).toBeTruthy(); + + fireEvent.click(showJoinedOrgsBtn); + fireEvent.click(screen.getByTestId(`removeUserFromOrgBtn${'abc'}`)); + const confirmRemoveBtn = screen.getByTestId(`confirmRemoveUser123`); + expect(confirmRemoveBtn).toBeInTheDocument(); + + fireEvent.click(confirmRemoveBtn); + }); + + test('Remove user from Organization should function properly in Organizations Blocked by Modal', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + userType: 'SUPERADMIN', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + { + _id: 'xyz', + }, + ], + createdAt: '2022-09-29T15:39:36.355Z', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'Blocked Organization 1', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=Blocked%20Organization%201', + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + }, + }, + { + _id: 'mno', + name: 'Blocked Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: null, + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'def', + name: 'Joined Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-19T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + }, + index: 0, + loggedInUserId: '123', + resetAndRefetch: resetAndRefetchMock, + }; + + render( + + + + + + ); + + await wait(); + const showBlockedByOrgsBtn = screen.getByTestId( + `showBlockedByOrgsBtn${123}` + ); + expect(showBlockedByOrgsBtn).toBeInTheDocument(); + fireEvent.click(showBlockedByOrgsBtn); + expect(screen.getByTestId('modal-blocked-org-123')).toBeInTheDocument(); + fireEvent.click(showBlockedByOrgsBtn); + fireEvent.click(screen.getByTestId(`removeUserFromOrgBtn${'xyz'}`)); + expect(screen.getByTestId('modal-remove-user-123')).toBeInTheDocument(); + + // Close using escape key and reopen + fireEvent.keyDown(screen.getByTestId('modal-blocked-org-123'), { + key: 'Escape', + code: 'Escape', + keyCode: 27, + charCode: 27, + }); + expect( + screen + .queryAllByRole('dialog') + .some((el) => el.className.includes('show')) + ).toBeTruthy(); + fireEvent.click(showBlockedByOrgsBtn); + // Close using close button and reopen + fireEvent.click(screen.getByTestId('closeRemoveUserModal123')); + expect( + screen + .queryAllByRole('dialog') + .some((el) => el.className.includes('show')) + ).toBeTruthy(); + + fireEvent.click(showBlockedByOrgsBtn); + fireEvent.click(screen.getByTestId(`removeUserFromOrgBtn${'xyz'}`)); + const confirmRemoveBtn = screen.getByTestId(`confirmRemoveUser123`); + expect(confirmRemoveBtn).toBeInTheDocument(); + + fireEvent.click(confirmRemoveBtn); + }); + + test('Should be able to change userType of a user if not self', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + userType: 'USER', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + { + _id: 'xyz', + }, + ], + createdAt: '2022-09-29T15:39:36.355Z', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'Blocked Organization 1', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=Blocked%20Organization%201', + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + }, + }, + { + _id: 'mno', + name: 'Blocked Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: null, + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'def', + name: 'Joined Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-19T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + }, + index: 0, + loggedInUserId: '456', + resetAndRefetch: resetAndRefetchMock, + }; + + render( + + + + + + ); + + await wait(); + fireEvent.select(screen.getByTestId(`changeRole123`), { + target: { value: 'ADMIN?123' }, + }); + }); + + test('Should be not able to change userType of self', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + userType: 'ADMIN', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + { + _id: 'xyz', + }, + ], + createdAt: '2022-09-29T15:39:36.355Z', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'Blocked Organization 1', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=Blocked%20Organization%201', + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + }, + }, + { + _id: 'mno', + name: 'Blocked Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: null, + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'def', + name: 'Joined Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-19T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + }, + index: 0, + loggedInUserId: '123', + resetAndRefetch: resetAndRefetchMock, + }; + + render( + + + + + + ); + + await wait(); + expect(screen.getByTestId(`changeRole123`)).toBeDisabled(); + expect(screen.getByTestId(`changeRole123`)).toHaveValue('ADMIN?123'); + }); +}); diff --git a/src/components/UsersTableItem/UserTableItemMocks.ts b/src/components/UsersTableItem/UserTableItemMocks.ts new file mode 100644 index 0000000000..6fd90d2be4 --- /dev/null +++ b/src/components/UsersTableItem/UserTableItemMocks.ts @@ -0,0 +1,59 @@ +import { + REMOVE_MEMBER_MUTATION, + UPDATE_USERTYPE_MUTATION, + UPDATE_USER_ROLE_IN_ORG_MUTATION, +} from 'GraphQl/Mutations/mutations'; + +export const MOCKS = [ + { + request: { + query: UPDATE_USERTYPE_MUTATION, + variables: { + id: '123', + userType: 'ADMIN', + }, + }, + result: { + data: { + updateUserType: { + data: { + id: '123', + }, + }, + }, + }, + }, + { + request: { + query: REMOVE_MEMBER_MUTATION, + variables: { + userid: '123', + orgid: 'abc', + }, + }, + result: { + data: { + removeUserFromOrganization: { + _id: '123', + }, + }, + }, + }, + { + request: { + query: UPDATE_USER_ROLE_IN_ORG_MUTATION, + variables: { + userId: '123', + organizationId: 'abc', + role: 'ADMIN', + }, + }, + result: { + data: { + updateUserRoleInOrganization: { + _id: '123', + }, + }, + }, + }, +]; diff --git a/src/components/UsersTableItem/UsersTableItem.module.css b/src/components/UsersTableItem/UsersTableItem.module.css new file mode 100644 index 0000000000..d5fad679a7 --- /dev/null +++ b/src/components/UsersTableItem/UsersTableItem.module.css @@ -0,0 +1,26 @@ +.input { + position: relative; +} + +.notJoined { + height: 300px; + display: flex; + justify-content: center; + align-items: center; +} + +.modalTable img[alt='creator'] { + height: 24px; + width: 24px; + object-fit: contain; + border-radius: 12px; + margin-right: 0.4rem; +} + +.modalTable img[alt='orgImage'] { + height: 28px; + width: 28px; + object-fit: contain; + border-radius: 4px; + margin-right: 0.4rem; +} diff --git a/src/components/UsersTableItem/UsersTableItem.tsx b/src/components/UsersTableItem/UsersTableItem.tsx new file mode 100644 index 0000000000..dbf51e809a --- /dev/null +++ b/src/components/UsersTableItem/UsersTableItem.tsx @@ -0,0 +1,598 @@ +import { useMutation } from '@apollo/client'; +import { Search } from '@mui/icons-material'; +import { + REMOVE_MEMBER_MUTATION, + UPDATE_USERTYPE_MUTATION, + UPDATE_USER_ROLE_IN_ORG_MUTATION, +} from 'GraphQl/Mutations/mutations'; +import dayjs from 'dayjs'; +import React, { useState } from 'react'; +import { Button, Form, Modal, Row, Table } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import { useHistory } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { errorHandler } from 'utils/errorHandler'; +import type { InterfaceQueryUserListItem } from 'utils/interfaces'; +import styles from './UsersTableItem.module.css'; + +type Props = { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; +}; + +const UsersTableItem = (props: Props): JSX.Element => { + const { t } = useTranslation('translation', { keyPrefix: 'users' }); + const { user, index, loggedInUserId, resetAndRefetch } = props; + + const [showJoinedOrganizations, setShowJoinedOrganizations] = useState(false); + const [showBlockedOrganizations, setShowBlockedOrganizations] = + useState(false); + const [showRemoveUserModal, setShowRemoveUserModal] = useState(false); + const [removeUserProps, setremoveUserProps] = useState<{ + orgName: string; + orgId: string; + setShowOnCancel: 'JOINED' | 'BLOCKED' | ''; + }>({ + orgName: '', + orgId: '', + setShowOnCancel: '', + }); + const [joinedOrgs, setJoinedOrgs] = useState(user.joinedOrganizations); + const [orgsBlockedBy, setOrgsBlockedBy] = useState( + user.organizationsBlockedBy + ); + const [searchByNameJoinedOrgs, setSearchByNameJoinedOrgs] = useState(''); + const [searchByNameOrgsBlockedBy, setSearchByNameOrgsBlockedBy] = + useState(''); + const [updateUserType] = useMutation(UPDATE_USERTYPE_MUTATION); + const [removeUser] = useMutation(REMOVE_MEMBER_MUTATION); + const [updateUserInOrgType] = useMutation(UPDATE_USER_ROLE_IN_ORG_MUTATION); + const history = useHistory(); + + /* istanbul ignore next */ + const changeRole = async (e: any): Promise => { + const { value } = e.target; + + const inputData = value.split('?'); + + try { + const { data } = await updateUserType({ + variables: { + id: inputData[1], + userType: inputData[0], + }, + }); + if (data) { + toast.success(t('roleUpdated')); + resetAndRefetch(); + } + } catch (error: any) { + /* istanbul ignore next */ + errorHandler(t, error); + } + }; + + const confirmRemoveUser = async (): Promise => { + try { + const { data } = await removeUser({ + variables: { + userid: user._id, + orgid: removeUserProps.orgId, + }, + }); + + if (data) { + toast.success('Removed User from Organization successfully'); + resetAndRefetch(); + } + } catch (error: any) { + /* istanbul ignore next */ + errorHandler(t, error); + } + }; + + /* istanbul ignore next */ + const changeRoleInOrg = async (e: any): Promise => { + const { value } = e.target; + + const inputData = value.split('?'); + + try { + const { data } = await updateUserInOrgType({ + variables: { + userId: user._id, + role: inputData[0], + organizationId: inputData[1], + }, + }); + if (data) { + toast.success(t('roleUpdated')); + resetAndRefetch(); + } + } catch (error: any) { + /* istanbul ignore next */ + errorHandler(t, error); + } + }; + + function goToOrg(_id: string): void { + const url = '/orgdash/id=' + _id; + + // Dont change the below two lines + window.location.replace(url); + history.push(url); + } + function handleCreator(): void { + toast.success('Profile Page Coming Soon !'); + } + function handleSearchJoinedOrgs(e: any): void { + const { value } = e.target; + setSearchByNameJoinedOrgs(value); + if (value == '') { + setJoinedOrgs(user.joinedOrganizations); + } else { + const filteredOrgs = user.joinedOrganizations.filter((org) => + org.name.toLowerCase().includes(value.toLowerCase()) + ); + setJoinedOrgs(filteredOrgs); + } + } + function handleSearcgByOrgsBlockedBy(e: any): void { + const { value } = e.target; + setSearchByNameOrgsBlockedBy(value); + if (value == '') { + setOrgsBlockedBy(user.organizationsBlockedBy); + } else { + const filteredOrgs = user.organizationsBlockedBy.filter((org) => + org.name.toLowerCase().includes(value.toLowerCase()) + ); + setOrgsBlockedBy(filteredOrgs); + } + } + + /* istanbul ignore next */ + function onHideRemoveUserModal(): void { + setShowRemoveUserModal(false); + if (removeUserProps.setShowOnCancel == 'JOINED') { + setShowJoinedOrganizations(true); + } else if (removeUserProps.setShowOnCancel == 'BLOCKED') { + setShowBlockedOrganizations(true); + } + } + return ( + <> + {/* Table Item */} + + + + + + + + + + {/* Organizations joined modal */} + setShowJoinedOrganizations(false)} + > + + + {t('orgJoinedBy')} {`${user.firstName}`} {`${user.lastName}`} ( + {user.joinedOrganizations.length}) + + + + {user.joinedOrganizations.length !== 0 && ( +
+ + +
+ )} + + {user.joinedOrganizations.length == 0 ? ( +
+

+ {user.firstName} {user.lastName} {t('hasNotJoinedAnyOrg')} +

+
+ ) : joinedOrgs.length == 0 ? ( + <> +
+

+ {t('noResultsFoundFor')} "{searchByNameJoinedOrgs} + " +

+
+ + ) : ( +
{title}{title}; + })}
-
-
+
+
{index + 1}{`${user.firstName} ${user.lastName}`}{user.email} + + + + + + + + + +
+ + + + + + + + + + + + + {joinedOrgs.map((org) => { + // Check user is admin for this organization or not + let isAdmin = false; + user.adminFor.map((item) => { + if (item._id == org._id) { + isAdmin = true; + } + }); + return ( + + + + + + + + + + ); + })} + +
NameLocationCreated onCreated ByUsers RoleChange RoleAction
+ + {org.location}{dayjs(org.createdAt).format('DD-MM-YYYY')} + + {isAdmin ? 'ADMIN' : 'USER'} + + {isAdmin ? ( + <> + + + + ) : ( + <> + + + + )} + + + +
+ )} + + + + + + + {/* Organizations blocked by modal */} + setShowBlockedOrganizations(false)} + data-testid={`modal-blocked-org-${user._id}`} + > + + + {t('orgThatBlocked')} {`${user.firstName}`} {`${user.lastName}`} ( + {user.organizationsBlockedBy.length}) + + + + {user.organizationsBlockedBy.length !== 0 && ( +
+ + +
+ )} + + {user.organizationsBlockedBy.length == 0 ? ( +
+

+ {user.firstName} {user.lastName} {t('isNotBlockedByAnyOrg')} +

+
+ ) : orgsBlockedBy.length == 0 ? ( + <> +
+

+ {t('noResultsFoundFor')} "{searchByNameOrgsBlockedBy} + " +

+
+ + ) : ( + + + + + + + + + + + + + + + {orgsBlockedBy.map((org) => { + // Check user is admin for this organization or not + let isAdmin = false; + user.adminFor.map((item) => { + if (item._id == org._id) { + isAdmin = true; + } + }); + return ( + + + + + + + + + + ); + })} + +
NameLocationCreated onCreated ByUsers RoleChange RoleAction
+ + {org.location}{dayjs(org.createdAt).format('DD-MM-YYYY')} + + {isAdmin ? 'ADMIN' : 'USER'} + + {isAdmin ? ( + <> + + + + ) : ( + <> + + + + )} + + + +
+ )} +
+
+ + + +
+ {/* Remove user from Organization modal */} + onHideRemoveUserModal()} + > + + + Remove User from {removeUserProps.orgName} + + + +

+ Are you sure you want to remove{' '} + + “{user.firstName} {user.lastName}” + {' '} + from organization{' '} + + “ + {removeUserProps.orgName}” + {' '} + ? +

+
+ + + + +
+ + ); +}; + +export default UsersTableItem; diff --git a/src/screens/ForgotPassword/ForgotPassword.module.css b/src/screens/ForgotPassword/ForgotPassword.module.css index e69de29bb2..74e09aecc6 100644 --- a/src/screens/ForgotPassword/ForgotPassword.module.css +++ b/src/screens/ForgotPassword/ForgotPassword.module.css @@ -0,0 +1,71 @@ +.pageWrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; +} + +.cardBody { + padding: 2rem; + background-color: #fff; + border-radius: 0.8rem; + border: 1px solid var(--bs-gray-200); +} + +.keyWrapper { + height: 72px; + width: 72px; + transform: rotate(180deg); + position: relative; + overflow: hidden; + display: block; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + margin: 1rem auto; +} + +.keyWrapper .themeOverlay { + position: absolute; + background-color: var(--bs-primary); + height: 100%; + width: 100%; + opacity: 0.15; +} + +.keyWrapper .keyLogo { + height: 42px; + width: 42px; + -webkit-animation: zoomIn 0.3s ease-in-out; + animation: zoomIn 0.3s ease-in-out; +} + +@-webkit-keyframes zoomIn { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5); + } + + 100% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@keyframes zoomIn { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5); + } + + 100% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } +} diff --git a/src/screens/ForgotPassword/ForgotPassword.test.tsx b/src/screens/ForgotPassword/ForgotPassword.test.tsx index 0c61fa90ec..94db5b2839 100644 --- a/src/screens/ForgotPassword/ForgotPassword.test.tsx +++ b/src/screens/ForgotPassword/ForgotPassword.test.tsx @@ -1,21 +1,21 @@ import React from 'react'; import { MockedProvider } from '@apollo/react-testing'; import { act, render, screen } from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; import userEvent from '@testing-library/user-event'; import 'jest-localstorage-mock'; import 'jest-location-mock'; import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; -import ForgotPassword from './ForgotPassword'; -import { store } from 'state/store'; import { FORGOT_PASSWORD_MUTATION, GENERATE_OTP_MUTATION, } from 'GraphQl/Mutations/mutations'; -import i18nForTest from 'utils/i18nForTest'; +import { store } from 'state/store'; import { StaticMockLink } from 'utils/StaticMockLink'; +import i18nForTest from 'utils/i18nForTest'; +import ForgotPassword from './ForgotPassword'; const MOCKS = [ { @@ -57,7 +57,9 @@ async function wait(ms = 100): Promise { }); }); } - +beforeEach(() => { + localStorage.setItem('IsLoggedIn', 'FALSE'); +}); afterEach(() => { localStorage.clear(); }); @@ -81,7 +83,9 @@ describe('Testing Forgot Password screen', () => { await wait(); expect(screen.getByText(/Forgot Password/i)).toBeInTheDocument(); - expect(screen.getByText(/Back to Home/i)).toBeInTheDocument(); + expect(screen.getByText(/Registered Email/i)).toBeInTheDocument(); + expect(screen.getByText(/Get Otp/i)).toBeInTheDocument(); + expect(screen.getByText(/Back to Login/i)).toBeInTheDocument(); expect(window.location).toBeAt('/orglist'); }); @@ -128,6 +132,7 @@ describe('Testing Forgot Password screen', () => { ); userEvent.click(screen.getByText('Get OTP')); + await wait(); }); test('Testing forgot password functionality', async () => { @@ -135,6 +140,7 @@ describe('Testing Forgot Password screen', () => { userOtp: '12345', newPassword: 'johnDoe', confirmNewPassword: 'johnDoe', + email: 'johndoe@gmail.com', }; render( @@ -151,18 +157,28 @@ describe('Testing Forgot Password screen', () => { await wait(); + userEvent.type( + screen.getByPlaceholderText(/Registered email/i), + formData.email + ); + + userEvent.click(screen.getByText('Get OTP')); + await wait(); + userEvent.type(screen.getByPlaceholderText('e.g. 12345'), formData.userOtp); userEvent.type(screen.getByTestId('newPassword'), formData.newPassword); userEvent.type( screen.getByTestId('confirmNewPassword'), formData.confirmNewPassword ); - + localStorage.setItem('otpToken', 'lorem ipsum'); userEvent.click(screen.getByText('Change Password')); + await wait(); }); test('Testing forgot password functionality, when new password and confirm password is not same', async () => { const formData = { + email: 'johndoe@gmail.com', userOtp: '12345', newPassword: 'johnDoe', confirmNewPassword: 'doeJohn', @@ -182,6 +198,14 @@ describe('Testing Forgot Password screen', () => { await wait(); + userEvent.type( + screen.getByPlaceholderText(/Registered email/i), + formData.email + ); + + userEvent.click(screen.getByText('Get OTP')); + await wait(); + userEvent.type(screen.getByPlaceholderText('e.g. 12345'), formData.userOtp); userEvent.type(screen.getByTestId('newPassword'), formData.newPassword); userEvent.type( @@ -197,6 +221,7 @@ describe('Testing Forgot Password screen', () => { userOtp: '12345', newPassword: 'johnDoe', confirmNewPassword: 'johnDoe', + email: 'johndoe@gmail.com', }; localStorage.setItem('otpToken', ''); @@ -215,13 +240,20 @@ describe('Testing Forgot Password screen', () => { await wait(); + userEvent.type( + screen.getByPlaceholderText(/Registered email/i), + formData.email + ); + + userEvent.click(screen.getByText('Get OTP')); + await wait(); + userEvent.type(screen.getByPlaceholderText('e.g. 12345'), formData.userOtp); userEvent.type(screen.getByTestId('newPassword'), formData.newPassword); userEvent.type( screen.getByTestId('confirmNewPassword'), formData.confirmNewPassword ); - userEvent.click(screen.getByText('Change Password')); }); }); diff --git a/src/screens/ForgotPassword/ForgotPassword.tsx b/src/screens/ForgotPassword/ForgotPassword.tsx index eb96f70f26..10b4c4db0b 100644 --- a/src/screens/ForgotPassword/ForgotPassword.tsx +++ b/src/screens/ForgotPassword/ForgotPassword.tsx @@ -1,6 +1,6 @@ +import { useMutation } from '@apollo/client'; import type { ChangeEvent } from 'react'; import React, { useEffect, useState } from 'react'; -import { useMutation } from '@apollo/client'; import { Link } from 'react-router-dom'; import { toast } from 'react-toastify'; @@ -8,13 +8,15 @@ import { FORGOT_PASSWORD_MUTATION, GENERATE_OTP_MUTATION, } from 'GraphQl/Mutations/mutations'; +import { ReactComponent as KeyLogo } from 'assets/svgs/key.svg'; -import styles from './ForgotPassword.module.css'; +import ArrowRightAlt from '@mui/icons-material/ArrowRightAlt'; +import Loader from 'components/Loader/Loader'; +import { Form } from 'react-bootstrap'; +import Button from 'react-bootstrap/Button'; import { useTranslation } from 'react-i18next'; import { errorHandler } from 'utils/errorHandler'; -import Button from 'react-bootstrap/Button'; -import { Form } from 'react-bootstrap'; -import Loader from 'components/Loader/Loader'; +import styles from './ForgotPassword.module.css'; const ForgotPassword = (): JSX.Element => { const { t } = useTranslation('translation', { @@ -23,8 +25,10 @@ const ForgotPassword = (): JSX.Element => { document.title = t('title'); - const [componentLoader, setComponentLoader] = useState(true); + const [showEnterEmail, setShowEnterEmail] = useState(true); + const [registeredEmail, setregisteredEmail] = useState(''); + const [forgotPassFormData, setForgotPassFormData] = useState({ userOtp: '', newPassword: '', @@ -35,13 +39,14 @@ const ForgotPassword = (): JSX.Element => { const [forgotPassword, { loading: forgotPasswordLoading }] = useMutation( FORGOT_PASSWORD_MUTATION ); - + const isLoggedIn = localStorage.getItem('IsLoggedIn'); useEffect(() => { - const isLoggedIn = localStorage.getItem('IsLoggedIn'); if (isLoggedIn == 'TRUE') { window.location.replace('/orglist'); } - setComponentLoader(false); + return () => { + localStorage.removeItem('otpToken'); + }; }, []); const getOTP = async (e: ChangeEvent): Promise => { @@ -54,13 +59,12 @@ const ForgotPassword = (): JSX.Element => { }, }); - /* istanbul ignore next */ if (data) { localStorage.setItem('otpToken', data.otp.otpToken); toast.success(t('OTPsent')); + setShowEnterEmail(false); } } catch (error: any) { - /* istanbul ignore next */ if (error.message === 'User not found') { toast.warn(t('emailNotRegistered')); } else if (error.message === 'Failed to fetch') { @@ -100,7 +104,7 @@ const ForgotPassword = (): JSX.Element => { /* istanbul ignore next */ if (data) { toast.success(t('passwordChanges')); - + setShowEnterEmail(true); setForgotPassFormData({ userOtp: '', newPassword: '', @@ -108,150 +112,135 @@ const ForgotPassword = (): JSX.Element => { }); } } catch (error: any) { + setShowEnterEmail(true); /* istanbul ignore next */ errorHandler(t, error); } }; - if (componentLoader || otpLoading || forgotPasswordLoading) { + if (otpLoading || forgotPasswordLoading) { return ; } - return ( -
-
-
-
-

{t('forgotPassword')}

-
- -
-
-
- -
- setregisteredEmail(e.target.value)} - /> -
-
- -
+ <> +
+
+
+
+
+
+
- -
- -
-
-
- -
- - setForgotPassFormData({ - ...forgotPassFormData, - userOtp: e.target.value, - }) - } - /> +

{t('forgotPassword')}

+ {showEnterEmail ? ( +
+ + + {t('registeredEmail')}: + +
+ + setregisteredEmail(e.target.value) + } + /> +
+ +
-
-
- -
- - setForgotPassFormData({ - ...forgotPassFormData, - newPassword: e.target.value, - }) - } - /> + ) : ( +
+
+ {t('enterOtp')}: + + setForgotPassFormData({ + ...forgotPassFormData, + userOtp: e.target.value, + }) + } + /> + + {t('enterNewPassword')}: + + + setForgotPassFormData({ + ...forgotPassFormData, + newPassword: e.target.value, + }) + } + /> + + {t('cofirmNewPassword')}: + + + setForgotPassFormData({ + ...forgotPassFormData, + confirmNewPassword: e.target.value, + }) + } + /> + +
-
-
- -
- - setForgotPassFormData({ - ...forgotPassFormData, - confirmNewPassword: e.target.value, - }) - } + -
-
-
- -
-
- - {t('backToHome')} + {t('backToLogin')}
- +
-
+ ); }; diff --git a/src/screens/LoginPage/LoginPage.module.css b/src/screens/LoginPage/LoginPage.module.css index b66db017d8..8e8e314ba9 100644 --- a/src/screens/LoginPage/LoginPage.module.css +++ b/src/screens/LoginPage/LoginPage.module.css @@ -16,13 +16,20 @@ } .row .right_portion { - height: 100vh; + min-height: 100vh; position: relative; overflow-y: scroll; + display: flex; + flex-direction: column; + justify-content: center; padding: 1rem 2.5rem; background: var(--bs-white); } +.row .right_portion::-webkit-scrollbar { + display: none; +} + .row .right_portion .langChangeBtn { margin: 0; position: absolute; @@ -35,6 +42,8 @@ width: 150px; display: block; margin: 1rem auto; + -webkit-animation: zoomIn 0.3s ease-in-out; + animation: zoomIn 0.3s ease-in-out; } .row .orText { @@ -83,7 +92,8 @@ height: 70px; width: unset; position: absolute; - top: 0.25rem; + margin: 0.5rem; + top: 0; right: 0; z-index: 100; } @@ -100,28 +110,62 @@ } .row .right_portion .langChangeBtn { - position: relative; - margin: 0; + position: absolute; + margin: 1rem; left: 0; top: 0; } + .marginTopForReg { + margin-top: 4rem !important; + } + .row .right_portion .talawa_logo { height: 120px; - margin: 0.75rem auto; + margin: 0 auto 2rem auto; } } + .active_tab { -webkit-animation: fadeIn 0.3s ease-in-out; animation: fadeIn 0.3s ease-in-out; } +@-webkit-keyframes zoomIn { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5); + } + + 100% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@keyframes zoomIn { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5); + } + + 100% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } +} + @-webkit-keyframes fadeIn { 0% { opacity: 0; -webkit-transform: translateY(2rem); transform: translateY(2rem); } + 100% { opacity: 1; -webkit-transform: translateY(0); @@ -135,6 +179,7 @@ -webkit-transform: translateY(2rem); transform: translateY(2rem); } + 100% { opacity: 1; -webkit-transform: translateY(0); diff --git a/src/screens/LoginPage/LoginPage.tsx b/src/screens/LoginPage/LoginPage.tsx index fe27ee9aed..a7f4029b92 100644 --- a/src/screens/LoginPage/LoginPage.tsx +++ b/src/screens/LoginPage/LoginPage.tsx @@ -228,7 +228,11 @@ function loginPage(): JSX.Element { - + {/* LOGIN FORM */}
input { cursor: pointer; color: #707070; } + .modalbody { width: 50px; } + .pluginStoreBtnContainer { display: flex; gap: 1rem; } + .greenregbtn { margin: 1rem 0 0; margin-top: 10px; @@ -200,6 +206,7 @@ form > input { width: 45%; height: 18px; } + .secondbtn { display: flex; align-items: center; diff --git a/src/screens/OrgList/OrgList.tsx b/src/screens/OrgList/OrgList.tsx index a34a084a14..d2229b2339 100644 --- a/src/screens/OrgList/OrgList.tsx +++ b/src/screens/OrgList/OrgList.tsx @@ -8,12 +8,15 @@ import { USER_ORGANIZATION_LIST, } from 'GraphQl/Queries/Queries'; import OrgListCard from 'components/OrgListCard/OrgListCard'; +import SuperAdminScreen from 'components/SuperAdminScreen/SuperAdminScreen'; import type { ChangeEvent } from 'react'; import React, { useEffect, useState } from 'react'; import { Col, Dropdown, Form, Row } from 'react-bootstrap'; import Button from 'react-bootstrap/Button'; import Modal from 'react-bootstrap/Modal'; import { useTranslation } from 'react-i18next'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import { Link } from 'react-router-dom'; import { toast } from 'react-toastify'; import convertToBase64 from 'utils/convertToBase64'; import debounce from 'utils/debounce'; @@ -24,8 +27,6 @@ import type { InterfaceUserType, } from 'utils/interfaces'; import styles from './OrgList.module.css'; -import SuperAdminScreen from 'components/SuperAdminScreen/SuperAdminScreen'; -import { Link } from 'react-router-dom'; function orgList(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'orgList' }); @@ -48,6 +49,10 @@ function orgList(): JSX.Element { setdialogModalIsOpen(!dialogModalisOpen); document.title = t('title'); + const perPageResult = 8; + const [isLoading, setIsLoading] = useState(true); + const [hasMore, sethasMore] = useState(true); + const [isLoadingMore, setIsLoadingMore] = useState(false); const [searchByName, setSearchByName] = useState(''); const [showModal, setShowModal] = useState(false); const [formState, setFormState] = useState({ @@ -79,12 +84,19 @@ function orgList(): JSX.Element { loading, error: errorList, refetch: refetchOrgs, + fetchMore, }: { data: InterfaceOrgConnectionType | undefined; loading: boolean; error?: Error | undefined; refetch: any; + fetchMore: any; } = useQuery(ORGANIZATION_CONNECTION_LIST, { + variables: { + first: perPageResult, + skip: 0, + filter: searchByName, + }, notifyOnNetworkStatusChange: true, }); @@ -103,6 +115,14 @@ function orgList(): JSX.Element { }; }, []); + useEffect(() => { + if (loading && isLoadingMore == false) { + setIsLoading(true); + } else { + setIsLoading(false); + } + }, [loading]); + /* istanbul ignore next */ const isAdminForCurrentOrg = ( currentOrg: InterfaceOrgConnectionInfoType @@ -175,14 +195,66 @@ function orgList(): JSX.Element { window.location.assign('/'); } + /* istanbul ignore next */ + const resetAllParams = (): void => { + refetchOrgs({ + filter: '', + first: perPageResult, + skip: 0, + }); + sethasMore(true); + }; + + /* istanbul ignore next */ const handleSearchByName = (e: any): void => { const { value } = e.target; setSearchByName(value); + if (value == '') { + resetAllParams(); + return; + } refetchOrgs({ filter: value, }); }; + /* istanbul ignore next */ + const loadMoreOrganizations = (): void => { + console.log('loadMoreOrganizations'); + setIsLoadingMore(true); + fetchMore({ + variables: { + skip: orgsData?.organizationsConnection.length || 0, + }, + updateQuery: ( + prev: + | { organizationsConnection: InterfaceOrgConnectionType[] } + | undefined, + { + fetchMoreResult, + }: { + fetchMoreResult: + | { organizationsConnection: InterfaceOrgConnectionType[] } + | undefined; + } + ): + | { organizationsConnection: InterfaceOrgConnectionType[] } + | undefined => { + setIsLoadingMore(false); + if (!fetchMoreResult) return prev; + if (fetchMoreResult.organizationsConnection.length < perPageResult) { + sethasMore(false); + } + return { + organizationsConnection: [ + ...(prev?.organizationsConnection || []), + ...(fetchMoreResult.organizationsConnection || []), + ], + }; + }, + }); + }; + const debouncedHandleSearchByName = debounce(handleSearchByName); return ( <> @@ -244,8 +316,8 @@ function orgList(): JSX.Element { )}
- {/* Organizations List */} - {!loading && + {/* Text Infos for list */} + {!isLoading && ((orgsData?.organizationsConnection.length === 0 && searchByName.length == 0) || (userData && @@ -256,7 +328,7 @@ function orgList(): JSX.Element {

{t('noOrgErrorTitle')}

{t('noOrgErrorDescription')}
- ) : !loading && + ) : !isLoading && orgsData?.organizationsConnection.length == 0 && /* istanbul ignore next */ searchByName.length > 0 ? ( @@ -268,52 +340,87 @@ function orgList(): JSX.Element { ) : ( - <> - )} -
- {loading ? ( - <> - {[...Array(8)].map((_, index) => ( -
-
-
-
-
-
-
-
-
+ <> + + {[...Array(perPageResult)].map((_, index) => ( +
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- ))} - - ) : userData && userData.user.userType == 'SUPERADMIN' ? ( - orgsData?.organizationsConnection.map((item) => { - return ( -
- + ))} + + } + hasMore={hasMore} + className={styles.listBox} + data-testid="organizations-list" + endMessage={ +
+
{t('endOfResults')}
- ); - }) - ) : userData && - userData.user.userType == 'ADMIN' && - userData.user.adminFor.length > 0 ? ( - orgsData?.organizationsConnection.map((item) => { - if (isAdminForCurrentOrg(item)) { - return ( -
- -
- ); } - }) - ) : null} -
+ > + {isLoading ? ( + <> + {[...Array(perPageResult)].map((_, index) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} + + ) : userData && userData.user.userType == 'SUPERADMIN' ? ( + orgsData?.organizationsConnection.map((item, index) => { + return ( +
+ +
+ ); + }) + ) : ( + userData && + userData.user.userType == 'ADMIN' && + userData.user.adminFor.length > 0 && + orgsData?.organizationsConnection.map((item) => { + if (isAdminForCurrentOrg(item)) { + return ( +
+ +
+ ); + } + }) + )} + + + )} {/* Create Organization Modal */} {' '} + {/* Plugin Notification Modal after Org is Created */}
@@ -502,10 +610,8 @@ function orgList(): JSX.Element {
- {/* Plugin Notification after Org is Created */} ); } - export default orgList; diff --git a/src/screens/OrgList/OrgListMocks.ts b/src/screens/OrgList/OrgListMocks.ts index 419af22036..1a1ab30ab7 100644 --- a/src/screens/OrgList/OrgListMocks.ts +++ b/src/screens/OrgList/OrgListMocks.ts @@ -34,7 +34,7 @@ const organizations: InterfaceOrgConnectionInfoType[] = [ _id: '1', creator: { _id: 'xyz', firstName: 'John', lastName: 'Doe' }, image: '', - name: 'Akatsuki', + name: 'Palisadoes Foundation', createdAt: '02/02/2022', admins: [ { @@ -46,7 +46,7 @@ const organizations: InterfaceOrgConnectionInfoType[] = [ _id: '234', }, ], - location: 'Washington DC', + location: 'Jamaica', }, ]; @@ -80,6 +80,12 @@ const MOCKS = [ { request: { query: ORGANIZATION_CONNECTION_LIST, + variables: { + first: 8, + skip: 0, + filter: '', + }, + notifyOnNetworkStatusChange: true, }, result: { data: { @@ -101,6 +107,12 @@ const MOCKS_EMPTY = [ { request: { query: ORGANIZATION_CONNECTION_LIST, + variables: { + first: 8, + skip: 0, + filter: '', + }, + notifyOnNetworkStatusChange: true, }, result: { data: { @@ -124,6 +136,12 @@ const MOCKS_ADMIN = [ { request: { query: ORGANIZATION_CONNECTION_LIST, + variables: { + first: 8, + skip: 0, + filter: '', + }, + notifyOnNetworkStatusChange: true, }, result: { data: { @@ -142,4 +160,4 @@ const MOCKS_ADMIN = [ }, ]; -export { MOCKS, MOCKS_EMPTY, MOCKS_ADMIN }; +export { MOCKS, MOCKS_ADMIN, MOCKS_EMPTY }; diff --git a/src/screens/OrgSettings/OrgSettings.test.tsx b/src/screens/OrgSettings/OrgSettings.test.tsx index e722dc4a8d..a5323f39c5 100644 --- a/src/screens/OrgSettings/OrgSettings.test.tsx +++ b/src/screens/OrgSettings/OrgSettings.test.tsx @@ -111,7 +111,7 @@ describe('Organisation Settings Page', () => { expect(screen.getAllByText(/Delete Organization/i)).toHaveLength(3); expect( screen.getByText( - /By clicking on Delete organization button you will the organization will be permanently deleted along with its events, tags and all related data/i + /By clicking on Delete Organization button the organization will be permanently deleted along with its events, tags and all related data/i ) ).toBeInTheDocument(); expect(screen.getByText(/Other Settings/i)).toBeInTheDocument(); diff --git a/src/screens/OrgSettings/OrgSettings.tsx b/src/screens/OrgSettings/OrgSettings.tsx index d0df92b2c6..91a8874fa1 100644 --- a/src/screens/OrgSettings/OrgSettings.tsx +++ b/src/screens/OrgSettings/OrgSettings.tsx @@ -15,7 +15,7 @@ function orgSettings(): JSX.Element { }); document.title = t('title'); - const currentUrl = window.location.href.split('=')[1]; + const orgId = window.location.href.split('=')[1]; return ( <> @@ -29,7 +29,7 @@ function orgSettings(): JSX.Element {
- {currentUrl && } + {orgId && } diff --git a/src/screens/OrganizationDashboard/OrganizationDashboard.test.tsx b/src/screens/OrganizationDashboard/OrganizationDashboard.test.tsx index 61d3eebc60..f1560ee4f0 100644 --- a/src/screens/OrganizationDashboard/OrganizationDashboard.test.tsx +++ b/src/screens/OrganizationDashboard/OrganizationDashboard.test.tsx @@ -72,11 +72,10 @@ describe('Organisation Dashboard Page', () => { expect(screen.getAllByText('Events')).toHaveLength(2); expect(screen.getByText('Blocked Users')).toBeInTheDocument(); expect(screen.getByText('Requests')).toBeInTheDocument(); - expect(screen.getByText('Upcoming events')).toBeInTheDocument(); - expect(screen.getByText('Latest posts')).toBeInTheDocument(); + expect(screen.getByText('Upcoming Events')).toBeInTheDocument(); + expect(screen.getByText('Latest Posts')).toBeInTheDocument(); expect(screen.getByText('Membership requests')).toBeInTheDocument(); - expect(screen.getAllByText('View all')).toHaveLength(3); - + expect(screen.getAllByText('View All')).toHaveLength(3); // Checking if events are rendered expect(screen.getByText('Event 1')).toBeInTheDocument(); expect( @@ -118,10 +117,10 @@ describe('Organisation Dashboard Page', () => { expect(toast.success).toBeCalledWith('Coming soon!'); expect( - screen.getByText('No membership requests present') + screen.getByText(/No membership requests present/i) ).toBeInTheDocument(); - expect(screen.getByText('No upcoming events')).toBeInTheDocument(); - expect(screen.getByText('No posts present')).toBeInTheDocument(); + expect(screen.getByText(/No upcoming events/i)).toBeInTheDocument(); + expect(screen.getByText(/No posts present/i)).toBeInTheDocument(); }); test('Testing error scenario', async () => { diff --git a/src/screens/OrganizationDashboard/OrganizationDashboard.tsx b/src/screens/OrganizationDashboard/OrganizationDashboard.tsx index 87e538a538..27ab4a4e98 100644 --- a/src/screens/OrganizationDashboard/OrganizationDashboard.tsx +++ b/src/screens/OrganizationDashboard/OrganizationDashboard.tsx @@ -168,7 +168,9 @@ function organizationDashboard(): JSX.Element {
-
Upcoming events
+
+ {t('upcomingEvents')} +
@@ -187,7 +189,7 @@ function organizationDashboard(): JSX.Element { }) ) : upcomingEvents.length == 0 ? (
-
No upcoming events
+
{t('noUpcomingEvents')}
) : ( upcomingEvents.slice(0, 5).map((event) => { @@ -207,7 +209,7 @@ function organizationDashboard(): JSX.Element {
-
Latest posts
+
{t('latestPosts')}
@@ -226,7 +228,7 @@ function organizationDashboard(): JSX.Element { }) ) : postData?.postsByOrganization?.length == 0 ? (
-
No posts present
+
{t('noPostsPresent')}
) : ( postData?.postsByOrganization.slice(0, 5).map((post) => { @@ -247,7 +249,9 @@ function organizationDashboard(): JSX.Element {
-
Membership requests
+
+ {t('membershipRequests')} +
@@ -266,7 +270,7 @@ function organizationDashboard(): JSX.Element { }) ) : data?.organizations[0].membershipRequests.length == 0 ? (
-
No membership requests present
+
{t('noMembershipRequests')}
) : ( data?.organizations[0]?.membershipRequests diff --git a/src/screens/OrganizationPeople/OrganizationPeople.test.tsx b/src/screens/OrganizationPeople/OrganizationPeople.test.tsx index bfabccdb3f..605c01f785 100644 --- a/src/screens/OrganizationPeople/OrganizationPeople.test.tsx +++ b/src/screens/OrganizationPeople/OrganizationPeople.test.tsx @@ -455,6 +455,10 @@ async function wait(ms = 2): Promise { }); } +// TODO - REMOVE THE NEXT LINE IT IS TO SUPPRESS THE ERROR +// FOR THE FIRST TEST WHICH CAME OUT OF NOWHERE +console.error = jest.fn(); + describe('Organization People Page', () => { const searchData = { firstName: 'Aditya', diff --git a/src/screens/Requests/Requests.test.tsx b/src/screens/Requests/Requests.test.tsx index 7946a77f42..f8247905bc 100644 --- a/src/screens/Requests/Requests.test.tsx +++ b/src/screens/Requests/Requests.test.tsx @@ -1,228 +1,19 @@ import React from 'react'; import { MockedProvider } from '@apollo/react-testing'; import { act, render, screen } from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; -import { I18nextProvider } from 'react-i18next'; import 'jest-localstorage-mock'; import 'jest-location-mock'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; -import Requests from './Requests'; -import { - ACCEPT_ADMIN_MUTATION, - REJECT_ADMIN_MUTATION, -} from 'GraphQl/Mutations/mutations'; -import { - ORGANIZATION_CONNECTION_LIST, - USER_LIST, - USER_ORGANIZATION_LIST, -} from 'GraphQl/Queries/Queries'; -import { store } from 'state/store'; import userEvent from '@testing-library/user-event'; -import i18nForTest from 'utils/i18nForTest'; -import { StaticMockLink } from 'utils/StaticMockLink'; import { ToastContainer } from 'react-toastify'; - -const MOCKS = [ - { - request: { - query: USER_ORGANIZATION_LIST, - variables: { id: localStorage.getItem('id') }, - }, - result: { - data: { - user: { - userType: 'SUPERADMIN', - firstName: 'John', - lastName: 'Doe', - image: '', - email: 'John_Does_Palasidoes@gmail.com', - adminFor: { - _id: 1, - name: 'Akatsuki', - image: '', - }, - }, - }, - }, - }, - { - request: { - query: USER_LIST, - }, - result: { - data: { - users: [ - { - _id: '123', - firstName: 'John', - lastName: 'Doe', - image: 'dummyImage', - email: 'johndoe@gmail.com', - userType: 'SUPERADMIN', - adminApproved: true, - createdAt: '20/06/2022', - organizationsBlockedBy: [ - { - _id: '256', - name: 'ABC', - }, - ], - joinedOrganizations: [ - { - __typename: 'Organization', - _id: '6401ff65ce8e8406b8f07af1', - }, - ], - }, - { - _id: '456', - firstName: 'Sam', - lastName: 'Smith', - image: 'dummyImage', - email: 'samsmith@gmail.com', - userType: 'ADMIN', - adminApproved: false, - createdAt: '20/06/2022', - organizationsBlockedBy: [ - { - _id: '256', - name: 'ABC', - }, - ], - joinedOrganizations: [ - { - __typename: 'Organization', - _id: '6401ff65ce8e8406b8f07af2', - }, - ], - }, - { - _id: '789', - firstName: 'Peter', - lastName: 'Parker', - image: 'dummyImage', - email: 'peterparker@gmail.com', - userType: 'USER', - adminApproved: true, - createdAt: '20/06/2022', - organizationsBlockedBy: [ - { - _id: '256', - name: 'ABC', - }, - ], - joinedOrganizations: [ - { - __typename: 'Organization', - _id: '6401ff65ce8e8406b8f07af3', - }, - ], - }, - ], - }, - }, - }, - { - request: { - query: ACCEPT_ADMIN_MUTATION, - variables: { - id: '123', - userType: 'ADMIN', - }, - }, - result: { - data: { - acceptAdmin: true, - }, - }, - }, - { - request: { - query: REJECT_ADMIN_MUTATION, - variables: { - id: '123', - userType: 'ADMIN', - }, - }, - result: { - data: { - rejectAdmin: true, - }, - }, - }, -]; - -const EMPTY_ORG_MOCKS = [ - { - request: { - query: ACCEPT_ADMIN_MUTATION, - variables: { - id: '123', - userType: 'ADMIN', - }, - }, - result: { - data: undefined, - }, - }, - { - request: { - query: REJECT_ADMIN_MUTATION, - variables: { - id: '123', - userType: 'ADMIN', - }, - }, - result: { - data: undefined, - }, - }, - { - request: { - query: ORGANIZATION_CONNECTION_LIST, - }, - result: { - data: { - organizationsConnection: [], - }, - }, - }, -]; - -const ORG_LIST_MOCK = [ - ...MOCKS, - { - request: { - query: ORGANIZATION_CONNECTION_LIST, - }, - result: { - data: { - organizationsConnection: [ - { - _id: 1, - image: '', - name: 'Akatsuki', - creator: { - firstName: 'John', - lastName: 'Doe', - }, - admins: [ - { - _id: '123', - }, - ], - members: { - _id: '234', - }, - createdAt: '02/02/2022', - location: 'Washington DC', - }, - ], - }, - }, - }, -]; +import { store } from 'state/store'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import i18nForTest from 'utils/i18nForTest'; +import Requests from './Requests'; +import { EMPTY_ORG_MOCKS, MOCKS, ORG_LIST_MOCK } from './RequestsMocks'; const link = new StaticMockLink(MOCKS, true); const link2 = new StaticMockLink(EMPTY_ORG_MOCKS, true); @@ -236,10 +27,19 @@ async function wait(ms = 100): Promise { }); } +beforeEach(() => { + localStorage.setItem('UserType', 'SUPERADMIN'); + localStorage.setItem('FirstName', 'John'); + localStorage.setItem('LastName', 'Doe'); +}); + +afterEach(() => { + localStorage.clear(); +}); + describe('Testing Request screen', () => { test('Component should be rendered properly', async () => { window.location.assign('/orglist'); - localStorage.setItem('UserType', 'SUPERADMIN'); render( diff --git a/src/screens/Requests/Requests.tsx b/src/screens/Requests/Requests.tsx index 4de82c71be..b0b0a5f6ad 100644 --- a/src/screens/Requests/Requests.tsx +++ b/src/screens/Requests/Requests.tsx @@ -1,5 +1,7 @@ +import React from 'react'; import { useMutation, useQuery } from '@apollo/client'; -import React, { useEffect, useState } from 'react'; +import type { ApolloError } from '@apollo/client'; +import { useEffect, useState } from 'react'; import { Dropdown, Form, Table } from 'react-bootstrap'; import Button from 'react-bootstrap/Button'; import { useTranslation } from 'react-i18next'; @@ -14,25 +16,30 @@ import { } from 'GraphQl/Mutations/mutations'; import { ORGANIZATION_CONNECTION_LIST, - USER_LIST, + USER_LIST_REQUEST, USER_ORGANIZATION_LIST, } from 'GraphQl/Queries/Queries'; import SuperAdminScreen from 'components/SuperAdminScreen/SuperAdminScreen'; +import TableLoader from 'components/TableLoader/TableLoader'; +import InfiniteScroll from 'react-infinite-scroll-component'; import debounce from 'utils/debounce'; import { errorHandler } from 'utils/errorHandler'; import type { InterfaceOrgConnectionType, + InterfaceQueryRequestListItem, InterfaceUserType, } from 'utils/interfaces'; import styles from './Requests.module.css'; -import TableLoader from 'components/TableLoader/TableLoader'; const Requests = (): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'requests' }); document.title = t('title'); - const [usersData, setUsersData] = useState([]); + const perPageResult = 12; + const [isLoading, setIsLoading] = useState(true); + const [hasMore, sethasMore] = useState(true); + const [isLoadingMore, setIsLoadingMore] = useState(false); const [searchByName, setSearchByName] = useState(''); const [acceptAdminFunc] = useMutation(ACCEPT_ADMIN_MUTATION); @@ -40,26 +47,40 @@ const Requests = (): JSX.Element => { const { data: currentUserData, }: { - data: InterfaceUserType | undefined; - loading: boolean; - error?: Error | undefined; + data?: InterfaceUserType; + error?: ApolloError; } = useQuery(USER_ORGANIZATION_LIST, { variables: { id: localStorage.getItem('id') }, }); const { - data: dataUsers, - loading: loadingUsers, + data: usersData, + loading: loading, + fetchMore, refetch: refetchUsers, - } = useQuery(USER_LIST, { + }: { + data?: { users: InterfaceQueryRequestListItem[] }; + loading: boolean; + fetchMore: any; + refetch: any; + error?: ApolloError; + } = useQuery(USER_LIST_REQUEST, { + variables: { + first: perPageResult, + skip: 0, + userType: 'ADMIN', + adminApproved: false, + firstName_contains: searchByName, + lastName_contains: '', + }, notifyOnNetworkStatusChange: true, }); const { data: dataOrgs, }: { - data: InterfaceOrgConnectionType | undefined; - error?: Error; + data?: InterfaceOrgConnectionType; + error?: ApolloError; } = useQuery(ORGANIZATION_CONNECTION_LIST); // To clear the search when the component is unmounted @@ -69,6 +90,16 @@ const Requests = (): JSX.Element => { }; }, []); + // To manage loading states + useEffect(() => { + if (!usersData) { + return; + } + if (usersData.users.length < perPageResult) { + sethasMore(false); + } + }, [usersData]); + // If the user is not Superadmin, redirect to Organizations screen useEffect(() => { const userType = localStorage.getItem('UserType'); @@ -87,17 +118,59 @@ const Requests = (): JSX.Element => { } }, [dataOrgs]); - // Set the usersData to the users that are not approved yet after every api call + // Manage the loading state useEffect(() => { - if (dataUsers) { - setUsersData( - dataUsers.users.filter( - (user: any) => - user.userType === 'ADMIN' && user.adminApproved === false - ) - ); + if (loading && isLoadingMore == false) { + setIsLoading(true); + } else { + setIsLoading(false); } - }, [dataUsers]); + }, [loading]); + + /* istanbul ignore next */ + const resetAndRefetch = (): void => { + refetchUsers({ + first: perPageResult, + skip: 0, + userType: 'ADMIN', + adminApproved: false, + firstName_contains: '', + lastName_contains: '', + }); + sethasMore(true); + }; + /* istanbul ignore next */ + const loadMoreRequests = (): void => { + setIsLoadingMore(true); + fetchMore({ + variables: { + skip: usersData?.users.length || 0, + userType: 'ADMIN', + adminApproved: false, + firstName_contains: searchByName, + lastName_contains: '', + }, + updateQuery: ( + prev: { users: InterfaceQueryRequestListItem[] } | undefined, + { + fetchMoreResult, + }: { + fetchMoreResult: + | { users: InterfaceQueryRequestListItem[] } + | undefined; + } + ): { users: InterfaceQueryRequestListItem[] } | undefined => { + setIsLoadingMore(false); + if (!fetchMoreResult) return prev; + if (fetchMoreResult.users.length < perPageResult) { + sethasMore(false); + } + return { + users: [...(prev?.users || []), ...(fetchMoreResult.users || [])], + }; + }, + }); + }; const acceptAdmin = async (userId: any): Promise => { try { @@ -110,7 +183,7 @@ const Requests = (): JSX.Element => { /* istanbul ignore next */ if (data) { toast.success(t('userApproved')); - setUsersData(usersData.filter((user: any) => user._id !== userId)); + resetAndRefetch(); } } catch (error: any) { /* istanbul ignore next */ @@ -129,7 +202,7 @@ const Requests = (): JSX.Element => { /* istanbul ignore next */ if (data) { toast.success(t('userRejected')); - setUsersData(usersData.filter((user: any) => user._id !== userId)); + resetAndRefetch(); } } catch (error: any) { /* istanbul ignore next */ @@ -137,10 +210,15 @@ const Requests = (): JSX.Element => { } }; - const handleSearchByName = (e: any): any => { + /* istanbul ignore next */ + const handleSearchByName = async (e: any): Promise => { const { value } = e.target; setSearchByName(value); - refetchUsers({ + if (value === '') { + resetAndRefetch(); + return; + } + await refetchUsers({ firstName_contains: value, lastName_contains: '', // Later on we can add several search and filter options @@ -217,82 +295,82 @@ const Requests = (): JSX.Element => {
- {loadingUsers == false && - usersData.length === 0 && + {isLoading == false && + usersData?.users.length === 0 && searchByName.length > 0 ? (

{t('noResultsFoundFor')} "{searchByName}"

- ) : loadingUsers == false && usersData.length === 0 ? ( + ) : isLoading == false && usersData?.users.length === 0 ? (

{t('noRequestFound')}

+ ) : isLoading ? ( + ) : ( -
- {loadingUsers ? ( - - ) : ( - - - - {headerTitles.map((title: string, index: number) => { - return ( - - ); - })} - - - - {usersData.map( - ( - user: { - _id: string; - firstName: string; - lastName: string; - email: string; - userType: string; - }, - index: number - ) => { - return ( - - - - - - - - ); - } - )} - -
- {title} -
{index + 1}{`${user.firstName} ${user.lastName}`}{user.email} - - - -
- )} -
+ } + hasMore={hasMore} + className={styles.listBox} + data-testid="organizations-list" + endMessage={ +
+
{t('endOfResults')}
+
+ } + > + + + + {headerTitles.map((title: string, index: number) => { + return ( + + ); + })} + + + + {usersData?.users && + usersData.users.map((user, index) => { + return ( + + + + + + + + ); + })} + +
+ {title} +
{index + 1}{`${user.firstName} ${user.lastName}`}{user.email} + + + +
+
)} diff --git a/src/screens/Requests/RequestsMocks.ts b/src/screens/Requests/RequestsMocks.ts new file mode 100644 index 0000000000..a57bff7c83 --- /dev/null +++ b/src/screens/Requests/RequestsMocks.ts @@ -0,0 +1,220 @@ +import { + ACCEPT_ADMIN_MUTATION, + REJECT_ADMIN_MUTATION, +} from 'GraphQl/Mutations/mutations'; +import { + ORGANIZATION_CONNECTION_LIST, + USER_LIST_REQUEST, + USER_ORGANIZATION_LIST, +} from 'GraphQl/Queries/Queries'; + +export const MOCKS = [ + { + request: { + query: USER_ORGANIZATION_LIST, + variables: { id: localStorage.getItem('id') }, + }, + result: { + data: { + user: { + _id: '123', + userType: 'SUPERADMIN', + firstName: 'John', + lastName: 'Doe', + image: '', + email: 'John_Does_Palasidoes@gmail.com', + adminFor: { + _id: 1, + name: 'Akatsuki', + image: '', + }, + }, + }, + }, + }, + { + request: { + query: USER_LIST_REQUEST, + variables: { + adminApproved: false, + first: 12, + firstName_contains: '', + lastName_contains: '', + skip: 0, + userType: 'ADMIN', + }, + notifyOnNetworkStatusChange: true, + }, + result: { + data: { + users: [ + { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: 'dummyImage', + email: 'johndoe@gmail.com', + userType: 'SUPERADMIN', + adminApproved: true, + createdAt: '20/06/2022', + organizationsBlockedBy: [ + { + _id: '256', + name: 'ABC', + }, + ], + joinedOrganizations: [ + { + __typename: 'Organization', + _id: '6401ff65ce8e8406b8f07af1', + }, + ], + }, + { + _id: '456', + firstName: 'Sam', + lastName: 'Smith', + image: 'dummyImage', + email: 'samsmith@gmail.com', + userType: 'ADMIN', + adminApproved: false, + createdAt: '20/06/2022', + organizationsBlockedBy: [ + { + _id: '256', + name: 'ABC', + }, + ], + joinedOrganizations: [ + { + __typename: 'Organization', + _id: '6401ff65ce8e8406b8f07af2', + }, + ], + }, + { + _id: '789', + firstName: 'Peter', + lastName: 'Parker', + image: 'dummyImage', + email: 'peterparker@gmail.com', + userType: 'USER', + adminApproved: true, + createdAt: '20/06/2022', + organizationsBlockedBy: [ + { + _id: '256', + name: 'ABC', + }, + ], + joinedOrganizations: [ + { + __typename: 'Organization', + _id: '6401ff65ce8e8406b8f07af3', + }, + ], + }, + ], + }, + }, + }, + { + request: { + query: ACCEPT_ADMIN_MUTATION, + variables: { + id: '123', + userType: 'ADMIN', + }, + }, + result: { + data: { + acceptAdmin: true, + }, + }, + }, + { + request: { + query: REJECT_ADMIN_MUTATION, + variables: { + id: '123', + userType: 'ADMIN', + }, + }, + result: { + data: { + rejectAdmin: true, + }, + }, + }, +]; + +export const EMPTY_ORG_MOCKS = [ + { + request: { + query: ACCEPT_ADMIN_MUTATION, + variables: { + id: '123', + userType: 'ADMIN', + }, + }, + result: { + data: undefined, + }, + }, + { + request: { + query: REJECT_ADMIN_MUTATION, + variables: { + id: '123', + userType: 'ADMIN', + }, + }, + result: { + data: undefined, + }, + }, + { + request: { + query: ORGANIZATION_CONNECTION_LIST, + }, + result: { + data: { + organizationsConnection: [], + }, + }, + }, +]; + +export const ORG_LIST_MOCK = [ + ...MOCKS, + { + request: { + query: ORGANIZATION_CONNECTION_LIST, + }, + result: { + data: { + organizationsConnection: [ + { + _id: 1, + image: '', + name: 'Akatsuki', + creator: { + firstName: 'John', + lastName: 'Doe', + }, + admins: [ + { + _id: '123', + }, + ], + members: { + _id: '234', + }, + createdAt: '02/02/2022', + location: 'Washington DC', + }, + ], + }, + }, + }, +]; diff --git a/src/screens/Users/Users.test.tsx b/src/screens/Users/Users.test.tsx index bc375bcc3d..03a65c49fd 100644 --- a/src/screens/Users/Users.test.tsx +++ b/src/screens/Users/Users.test.tsx @@ -9,199 +9,11 @@ import { BrowserRouter } from 'react-router-dom'; import { ToastContainer } from 'react-toastify'; import userEvent from '@testing-library/user-event'; -import { UPDATE_USERTYPE_MUTATION } from 'GraphQl/Mutations/mutations'; -import { - ORGANIZATION_CONNECTION_LIST, - USER_LIST, - USER_ORGANIZATION_LIST, -} from 'GraphQl/Queries/Queries'; import { store } from 'state/store'; import { StaticMockLink } from 'utils/StaticMockLink'; import i18nForTest from 'utils/i18nForTest'; import Users from './Users'; - -const MOCKS = [ - { - request: { - query: USER_ORGANIZATION_LIST, - variables: { id: 'SUPERADMIN' }, - }, - result: { - data: { - user: { - userType: 'SUPERADMIN', - firstName: 'John', - lastName: 'Doe', - image: '', - email: 'John_Does_Palasidoes@gmail.com', - adminFor: { - _id: 1, - name: 'Palisadoes', - image: '', - }, - }, - }, - }, - }, - { - request: { - query: USER_LIST, - }, - result: { - data: { - users: [ - { - _id: '123', - firstName: 'John', - lastName: 'Doe', - image: 'dummyImage', - email: 'johndoe@gmail.com', - userType: 'SUPERADMIN', - adminApproved: true, - createdAt: '20/06/2022', - organizationsBlockedBy: [ - { - _id: '256', - name: 'ABC', - }, - ], - joinedOrganizations: [ - { - __typename: 'Organization', - _id: '6401ff65ce8e8406b8f07af1', - }, - ], - }, - { - _id: '456', - firstName: 'Sam', - lastName: 'Smith', - image: 'dummyImage', - email: 'samsmith@gmail.com', - userType: 'ADMIN', - adminApproved: true, - createdAt: '20/06/2022', - organizationsBlockedBy: [ - { - _id: '256', - name: 'ABC', - }, - ], - joinedOrganizations: [ - { - __typename: 'Organization', - _id: '6401ff65ce8e8406b8f07af2', - }, - ], - }, - { - _id: '789', - firstName: 'Peter', - lastName: 'Parker', - image: 'dummyImage', - email: 'peterparker@gmail.com', - userType: 'USER', - adminApproved: true, - createdAt: '20/06/2022', - organizationsBlockedBy: [ - { - _id: '256', - name: 'ABC', - }, - ], - joinedOrganizations: [ - { - __typename: 'Organization', - _id: '6401ff65ce8e8406b8f07af3', - }, - ], - }, - ], - }, - }, - }, - { - request: { - query: UPDATE_USERTYPE_MUTATION, - variables: { - id: '123', - userType: 'ADMIN', - }, - }, - result: { - data: { - updateUserType: true, - }, - }, - }, - { - request: { - query: ORGANIZATION_CONNECTION_LIST, - }, - result: { - data: { - organizationsConnection: [ - { - _id: 1, - image: '', - name: 'Akatsuki', - creator: { - firstName: 'John', - lastName: 'Doe', - }, - admins: [ - { - _id: '123', - }, - ], - members: { - _id: '234', - }, - createdAt: '02/02/2022', - location: 'Washington DC', - }, - ], - }, - }, - }, -]; - -const EMPTY_MOCKS = [ - { - request: { - query: USER_LIST, - }, - result: { - data: { - users: [], - }, - }, - }, - { - request: { - query: UPDATE_USERTYPE_MUTATION, - variables: { - id: '123', - userType: 'ADMIN', - }, - }, - result: { - data: { - updateUserType: false, - }, - }, - }, - { - request: { - query: ORGANIZATION_CONNECTION_LIST, - }, - result: { - data: { - organizationsConnection: [], - }, - }, - }, -]; +import { EMPTY_MOCKS, MOCKS } from './UsersMocks'; const link = new StaticMockLink(MOCKS, true); const link2 = new StaticMockLink(EMPTY_MOCKS, true); @@ -213,11 +25,19 @@ async function wait(ms = 100): Promise { }); }); } +beforeEach(() => { + localStorage.setItem('id', '123'); + localStorage.setItem('UserType', 'SUPERADMIN'); + localStorage.setItem('FirstName', 'John'); + localStorage.setItem('LastName', 'Doe'); +}); + +afterEach(() => { + localStorage.clear(); +}); describe('Testing Users screen', () => { test('Component should be rendered properly', async () => { - window.location.assign('/orglist'); - render( @@ -232,19 +52,12 @@ describe('Testing Users screen', () => { await wait(); expect(screen.getAllByText(/Users/i)).toBeTruthy(); - expect(window.location).toBeAt('/orglist'); }); - test('Component should be rendered properly when user is not superAdmin', async () => { - const localStorageMock = { - getItem: jest.fn(() => 'ADMIN'), - }; - - Object.defineProperty(window, 'localStorage', { - value: localStorageMock, - writable: true, - }); - + test(`Component should be rendered properly when user is not superAdmin + and or userId does not exists in localstorage`, async () => { + localStorage.setItem('UserType', 'ADMIN'); + localStorage.setItem('id', ''); render( @@ -258,22 +71,11 @@ describe('Testing Users screen', () => { ); await wait(); - - expect(window.location.assign).toHaveBeenCalled(); }); test('Component should be rendered properly when user is superAdmin', async () => { - const localStorageMock = { - getItem: jest.fn(() => 'SUPERADMIN'), - }; - - Object.defineProperty(window, 'localStorage', { - value: localStorageMock, - writable: true, - }); - render( - + @@ -285,8 +87,6 @@ describe('Testing Users screen', () => { ); await wait(); - - expect(window.location.assign).not.toHaveBeenCalled(); }); test('Testing seach by name functionality', async () => { @@ -319,24 +119,7 @@ describe('Testing Users screen', () => { const search5 = 'Xe'; userEvent.type(screen.getByTestId(/searchByName/i), search5); - }); - - test('Testing change role functionality', async () => { - render( - - - - - - - - - - ); - - await wait(); - - userEvent.selectOptions(screen.getByTestId(/changeRole123/i), 'ADMIN'); + userEvent.type(screen.getByTestId(/searchByName/i), ''); }); test('Testing User data is not present', async () => { @@ -396,79 +179,4 @@ describe('Testing Users screen', () => { 'Organizations not found, please create an organization through dashboard' ); }); - - test('Should disable select when user is self', async () => { - const localStorageMock = (function (): any { - const store: any = { - UserType: 'SUPERADMIN', - id: '123', - }; - - return { - getItem: jest.fn((key: string) => store[key]), - }; - })(); - - Object.defineProperty(window, 'localStorage', { value: localStorageMock }); - - render( - - - - - - - - - - - ); - - await wait(); - - expect(screen.getByTestId('changeRole123')).toHaveProperty( - 'disabled', - true - ); - }); - - test('Should not disable select when user is not self', async () => { - const localStorageMock = (function (): any { - const store: any = { - UserType: 'SUPERADMIN', - id: '123', - }; - - return { - getItem: jest.fn((key: string) => store[key]), - }; - })(); - - Object.defineProperty(window, 'localStorage', { value: localStorageMock }); - - render( - - - - - - - - - - - ); - - await wait(); - - expect(screen.getByTestId('changeRole456')).toHaveProperty( - 'disabled', - false - ); - - expect(screen.getByTestId('changeRole789')).toHaveProperty( - 'disabled', - false - ); - }); }); diff --git a/src/screens/Users/Users.tsx b/src/screens/Users/Users.tsx index ccf70cb712..e7b1e0c1af 100644 --- a/src/screens/Users/Users.tsx +++ b/src/screens/Users/Users.tsx @@ -1,4 +1,5 @@ -import { useMutation, useQuery } from '@apollo/client'; +import type { ApolloError } from '@apollo/client'; +import { useQuery } from '@apollo/client'; import React, { useEffect, useState } from 'react'; import { Dropdown, Form, Table } from 'react-bootstrap'; import Button from 'react-bootstrap/Button'; @@ -11,45 +12,62 @@ import SortIcon from '@mui/icons-material/Sort'; import { ORGANIZATION_CONNECTION_LIST, USER_LIST, - USER_ORGANIZATION_LIST, } from 'GraphQl/Queries/Queries'; import SuperAdminScreen from 'components/SuperAdminScreen/SuperAdminScreen'; -import { errorHandler } from 'utils/errorHandler'; -import styles from './Users.module.css'; -import type { InterfaceUserType } from 'utils/interfaces'; -import { UPDATE_USERTYPE_MUTATION } from 'GraphQl/Mutations/mutations'; -import debounce from 'utils/debounce'; import TableLoader from 'components/TableLoader/TableLoader'; +import UsersTableItem from 'components/UsersTableItem/UsersTableItem'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import debounce from 'utils/debounce'; +import type { InterfaceQueryUserListItem } from 'utils/interfaces'; +import styles from './Users.module.css'; const Users = (): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'users' }); document.title = t('title'); + const perPageResult = 12; + const [isLoading, setIsLoading] = useState(true); + const [hasMore, setHasMore] = useState(true); + const [isLoadingMore, setIsLoadingMore] = useState(false); const [searchByName, setSearchByName] = useState(''); const userType = localStorage.getItem('UserType'); - const userId = localStorage.getItem('id'); + const loggedInUserId = localStorage.getItem('id'); + const { - data: currentUserData, + data: usersData, + loading: loading, + fetchMore, + refetch: refetchUsers, }: { - data: InterfaceUserType | undefined; + data?: { users: InterfaceQueryUserListItem[] }; loading: boolean; - error?: Error | undefined; - } = useQuery(USER_ORGANIZATION_LIST, { - variables: { id: localStorage.getItem('id') }, - }); - const { - data: dataUsers, - loading: loadingUsers, - refetch: refetchUsers, + fetchMore: any; + refetch: any; + error?: ApolloError; } = useQuery(USER_LIST, { + variables: { + first: perPageResult, + skip: 0, + firstName_contains: '', + lastName_contains: '', + }, notifyOnNetworkStatusChange: true, }); - const [updateUserType] = useMutation(UPDATE_USERTYPE_MUTATION); const { data: dataOrgs } = useQuery(ORGANIZATION_CONNECTION_LIST); + // Manage loading more state + useEffect(() => { + if (!usersData) { + return; + } + if (usersData.users.length < perPageResult) { + setHasMore(false); + } + }, [usersData]); + // To clear the search when the component is unmounted useEffect(() => { return () => { @@ -75,40 +93,67 @@ const Users = (): JSX.Element => { } }, []); - const changeRole = async (e: any): Promise => { - const { value } = e.target; - - const inputData = value.split('?'); - - try { - const { data } = await updateUserType({ - variables: { - id: inputData[1], - userType: inputData[0], - }, - }); - - /* istanbul ignore next */ - if (data) { - toast.success(t('roleUpdated')); - refetchUsers(); - } - } catch (error: any) { - /* istanbul ignore next */ - errorHandler(t, error); + // Manage the loading state + useEffect(() => { + if (loading && isLoadingMore == false) { + setIsLoading(true); + } else { + setIsLoading(false); } - }; + }, [loading]); const handleSearchByName = (e: any): void => { const { value } = e.target; setSearchByName(value); + /* istanbul ignore next */ + if (value.length === 0) { + resetAndRefetch(); + return; + } refetchUsers({ firstName_contains: value, lastName_contains: '', // Later on we can add several search and filter options }); }; - + /* istanbul ignore next */ + const resetAndRefetch = (): void => { + refetchUsers({ + first: perPageResult, + skip: 0, + firstName_contains: '', + lastName_contains: '', + }); + setHasMore(true); + }; + /* istanbul ignore next */ + const loadMoreUsers = (): void => { + setIsLoadingMore(true); + fetchMore({ + variables: { + skip: usersData?.users.length || 0, + userType: 'ADMIN', + filter: searchByName, + }, + updateQuery: ( + prev: { users: InterfaceQueryUserListItem[] } | undefined, + { + fetchMoreResult, + }: { + fetchMoreResult: { users: InterfaceQueryUserListItem[] } | undefined; + } + ): { users: InterfaceQueryUserListItem[] } | undefined => { + setIsLoadingMore(false); + if (!fetchMoreResult) return prev; + if (fetchMoreResult.users.length < perPageResult) { + setHasMore(false); + } + return { + users: [...(prev?.users || []), ...(fetchMoreResult.users || [])], + }; + }, + }); + }; const debouncedHandleSearchByName = debounce(handleSearchByName); const headerTitles: string[] = [ @@ -116,6 +161,8 @@ const Users = (): JSX.Element => { t('name'), t('email'), t('roles_userType'), + t('joined_organizations'), + t('blocked_organizations'), ]; return ( @@ -127,11 +174,7 @@ const Users = (): JSX.Element => {
{
- - {loadingUsers == false && - dataUsers && - dataUsers.users.length === 0 && + {isLoading == false && + usersData && + usersData.users.length === 0 && searchByName.length > 0 ? (

{t('noResultsFoundFor')} "{searchByName}"

- ) : loadingUsers == false && - dataUsers && - dataUsers.users.length === 0 ? ( + ) : isLoading == false && usersData && usersData.users.length === 0 ? ( // eslint-disable-next-line react/jsx-indent

{t('noUserFound')}

) : (
- {loadingUsers ? ( - + {isLoading ? ( + ) : ( - - - - {headerTitles.map((title: string, index: number) => { - return ( - - ); - })} - - - - {dataUsers && - dataUsers?.users.map( - ( - user: { - _id: string; - firstName: string; - lastName: string; - email: string; - userType: string; - }, - index: number - ) => { + + } + hasMore={hasMore} + className={styles.listBox} + data-testid="users-list" + endMessage={ +
+
{t('endOfResults')}
+
+ } + > +
- {title} -
+ + + {headerTitles.map((title: string, index: number) => { + return ( + + ); + })} + + + + {usersData && + usersData?.users.map((user, index) => { return ( - - - - - - + ); - } - )} - -
+ {title} +
{index + 1}{`${user.firstName} ${user.lastName}`}{user.email} - -
+ })} + + + )}
)} diff --git a/src/screens/Users/UsersMocks.ts b/src/screens/Users/UsersMocks.ts new file mode 100644 index 0000000000..2ee589fd2f --- /dev/null +++ b/src/screens/Users/UsersMocks.ts @@ -0,0 +1,216 @@ +import { + ORGANIZATION_CONNECTION_LIST, + USER_LIST, + USER_ORGANIZATION_LIST, +} from 'GraphQl/Queries/Queries'; + +export const MOCKS = [ + { + request: { + query: USER_ORGANIZATION_LIST, + variables: { id: 'user1' }, + }, + result: { + data: { + user: { + _id: 'user1', + userType: 'SUPERADMIN', + firstName: 'John', + lastName: 'Doe', + image: '', + email: 'John_Does_Palasidoes@gmail.com', + adminFor: [ + { + _id: 1, + name: 'Palisadoes', + image: '', + }, + ], + }, + }, + }, + }, + { + request: { + query: USER_LIST, + variables: { + first: 12, + skip: 0, + firstName_contains: '', + lastName_contains: '', + }, + }, + result: { + data: { + users: [ + { + _id: 'user1', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + userType: 'SUPERADMIN', + adminApproved: true, + adminFor: [ + { + _id: '123', + }, + ], + createdAt: '20/06/2022', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'ABC', + image: null, + location: 'Jamaica', + createdAt: '20/06/2022', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + createdAt: '20/06/2022', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: null, + location: 'Jamaica', + createdAt: '20/06/2022', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + createdAt: '20/06/2022', + }, + }, + ], + }, + { + _id: 'user2', + firstName: 'Jane', + lastName: 'Doe', + image: null, + email: 'john@example.com', + userType: 'SUPERADMIN', + adminApproved: true, + adminFor: [ + { + _id: '123', + }, + ], + createdAt: '20/06/2022', + organizationsBlockedBy: [ + { + _id: '456', + name: 'ABC', + image: null, + location: 'Jamaica', + createdAt: '20/06/2022', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + createdAt: '20/06/2022', + }, + }, + ], + joinedOrganizations: [ + { + _id: '123', + name: 'Palisadoes', + image: null, + location: 'Jamaica', + createdAt: '20/06/2022', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + createdAt: '20/06/2022', + }, + }, + ], + }, + ], + }, + }, + }, + { + request: { + query: ORGANIZATION_CONNECTION_LIST, + }, + result: { + data: { + organizationsConnection: [ + { + _id: 123, + image: null, + creator: { + firstName: 'John', + lastName: 'Doe', + }, + name: 'Palisadoes', + members: [ + { + _id: 'user1', + }, + { + _id: 'user2', + }, + ], + admins: [ + { + _id: 'user1', + }, + { + _id: 'user2', + }, + ], + createdAt: '09/11/2001', + location: 'Twin Tower', + }, + ], + }, + }, + }, +]; + +export const EMPTY_MOCKS = [ + { + request: { + query: USER_LIST, + + variables: { + first: 12, + skip: 0, + firstName_contains: '', + lastName_contains: '', + }, + }, + result: { + data: { + users: [], + }, + }, + }, + { + request: { + query: ORGANIZATION_CONNECTION_LIST, + }, + result: { + data: { + organizationsConnection: [], + }, + }, + }, +]; diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 304fc36715..1a41e91dfb 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -113,3 +113,54 @@ export interface InterfaceQueryBlockPageMemberListItem { _id: string; }[]; } + +export interface InterfaceQueryUserListItem { + _id: string; + firstName: string; + lastName: string; + image: string | null; + email: string; + userType: string; + adminFor: { _id: string }[]; + adminApproved: boolean; + organizationsBlockedBy: { + _id: string; + name: string; + location: string; + image: string | null; + createdAt: string; + creator: { + _id: string; + firstName: string; + lastName: string; + email: string; + image: string | null; + }; + }[]; + joinedOrganizations: { + _id: string; + name: string; + location: string; + image: string | null; + createdAt: string; + creator: { + _id: string; + firstName: string; + lastName: string; + email: string; + image: string | null; + }; + }[]; + createdAt: string; +} + +export interface InterfaceQueryRequestListItem { + _id: string; + firstName: string; + lastName: string; + image: string; + email: string; + userType: string; + adminApproved: boolean; + createdAt: string; +} From 40efb0f2b6a2883b94eb63d66d0f7bc5324616cb Mon Sep 17 00:00:00 2001 From: Sandeep Kumar Bhagat Date: Tue, 31 Oct 2023 22:08:16 +0530 Subject: [PATCH 2/2] fix createEvent to close modal and show events without refresh (#1014) --- src/components/EventCalendar/EventCalendar.tsx | 2 +- src/screens/OrganizationEvents/OrganizationEvents.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/EventCalendar/EventCalendar.tsx b/src/components/EventCalendar/EventCalendar.tsx index 336efd9882..4cb027647d 100644 --- a/src/components/EventCalendar/EventCalendar.tsx +++ b/src/components/EventCalendar/EventCalendar.tsx @@ -116,7 +116,7 @@ const Calendar: React.FC = ({ useEffect(() => { const data = filterData(eventData, orgData, userRole, userId); setEvents(data); - }, []); + }, [eventData, orgData, userRole, userId]); const handlePrevMonth = (): void => { if (currentMonth === 0) { diff --git a/src/screens/OrganizationEvents/OrganizationEvents.tsx b/src/screens/OrganizationEvents/OrganizationEvents.tsx index 566bda2e29..fe25bcdb6d 100644 --- a/src/screens/OrganizationEvents/OrganizationEvents.tsx +++ b/src/screens/OrganizationEvents/OrganizationEvents.tsx @@ -105,6 +105,7 @@ function organizationEvents(): JSX.Element { if (createEventData) { toast.success(t('eventCreated')); refetch(); + hideInviteModal(); setFormState({ title: '', eventdescrip: '',