From 066bb4e37d5354146d05dff6a60bfe8ba812aaeb Mon Sep 17 00:00:00 2001 From: Peter Harrison <16875803+palisadoes@users.noreply.github.com> Date: Thu, 26 Oct 2023 12:49:06 -0700 Subject: [PATCH 01/20] Update issue.yml --- .github/workflows/issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 44b435875f..06da465ccf 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -18,7 +18,7 @@ jobs: name: Adding Issue Label runs-on: ubuntu-latest steps: - - uses: Renato66/auto-label@v2.2.0 + - uses: Renato66/auto-label@v2.3.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} ignore-comments: true From 3a0e8ee2ddd45d4e127544caed97905c69f62740 Mon Sep 17 00:00:00 2001 From: Peter Harrison <16875803+palisadoes@users.noreply.github.com> Date: Fri, 27 Oct 2023 18:28:16 -0700 Subject: [PATCH 02/20] Update stale.yml --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 83f5eedc32..24667f8e06 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/stale@v7 + - uses: actions/stale@v8 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'This issue did not get any activity in the past 10 days and will be closed in 180 days if no update occurs. Please check if the develop branch has fixed it and report again or close the issue.' 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 03/20] 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 04/20] 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: '', From cf6bc4efc149c690aa7259bd047f65f4f84351f1 Mon Sep 17 00:00:00 2001 From: Akhilender Bongirwar <112749383+akhilender-bongirwar@users.noreply.github.com> Date: Sat, 4 Nov 2023 21:57:19 +0530 Subject: [PATCH 05/20] fix: Ensure Full Visibility of Logo on 404 Error Page (#1018) - Adjusted the positioning of the logo on the 404 error page to ensure full visibility. - Implemented CSS modifications to prevent the logo from being covered or cut off. - Tested the changes by navigating to various undefined endpoints, confirming that the logo is now displayed correctly on the 404 error page. This commit addresses the bug by ensuring the proper display of the logo on the 404 error page, enhancing the user experience. Fixes #1016 Signed-off-by: Akhilender --- src/screens/PageNotFound/PageNotFound.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/PageNotFound/PageNotFound.module.css b/src/screens/PageNotFound/PageNotFound.module.css index 5aa083ef15..3c1b9a3413 100644 --- a/src/screens/PageNotFound/PageNotFound.module.css +++ b/src/screens/PageNotFound/PageNotFound.module.css @@ -1,6 +1,6 @@ .notfound { position: relative; - bottom: 80px; + bottom: 20px; } .notfound h3 { From 5696ed82511cb5ea71d85183ae70befd16e7e71b Mon Sep 17 00:00:00 2001 From: Aashima wadhwa <73706697+aashimawadhwa@users.noreply.github.com> Date: Sat, 4 Nov 2023 22:41:56 +0530 Subject: [PATCH 06/20] Implemented featurs for Postfeed Management (#982) * redesigned newsfeed * added test * newsfeed management * pinned post * postfeed management * pin unpin tag and video control * changes part 2 * newsfeed changes * changes in Card Preview of newsfeed * changes in Card Preview of newsfeed * testing phase 1 * translation * tests * tests * merged intto develop * test * Added query --- public/locales/en.json | 15 +- public/locales/fr.json | 45 +- public/locales/hi.json | 49 +- public/locales/sp.json | 55 +- public/locales/zh.json | 61 +- src/GraphQl/Mutations/mutations.ts | 25 +- src/GraphQl/Queries/Queries.ts | 2 + .../OrgPostCard/OrgPostCard.module.css | 223 +++++- .../OrgPostCard/OrgPostCard.test.tsx | 327 +++++++-- src/components/OrgPostCard/OrgPostCard.tsx | 640 +++++++++++++----- src/screens/OrgPost/OrgPost.module.css | 190 +++--- src/screens/OrgPost/OrgPost.test.tsx | 377 +++++++++-- src/screens/OrgPost/OrgPost.tsx | 514 +++++++++----- src/screens/UserPortal/Home/Home.test.tsx | 6 + src/setupTests.ts | 4 + 15 files changed, 1865 insertions(+), 668 deletions(-) diff --git a/public/locales/en.json b/public/locales/en.json index 57f7b0d07b..6f3d16b201 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -268,17 +268,25 @@ "posts": "Posts", "createPost": "Create Post", "postDetails": "Post Details", + "postTitle1": "Write title of the post", "postTitle": "Title", "information": "Information", + "information1": "Write information of the post", "image": "Post Image", - "video": "Video", + "video": "Post Video", "addPost": "Add Post", "searchTitle": "Search By Title", "searchText": "Search By Text", "ptitle": "Post Title", "postDes": "What do you to talk about?", "Title": "Title", - "Text": "Text" + "Text": "Text", + "cancel": "Cancel", + "searchBy": "Search By", + "Oldest": "Oldest First", + "Latest": "Latest First", + "sortPost": "Sort Post", + "tag": " Your browser does not support the video tag" }, "postNotFound": { "post": "Post", @@ -300,6 +308,7 @@ "author": "Author", "imageURL": "Image URL", "videoURL": "Video URL", + "edit": "Edit Post", "deletePost": "Delete Post", "deletePostMsg": "Do you want to remove this post?", "no": "No", @@ -313,6 +322,8 @@ "updatePost": "Update Post", "postDeleted": "Post deleted successfully.", "postUpdated": "Post Updated successfully.", + "tag": " Your browser does not support the video tag", + "pin": "Pin Post", "talawaApiUnavailable": "Talawa-API service is unavailable. Is it running? Check your network connectivity too." }, "blockUnblockUser": { diff --git a/public/locales/fr.json b/public/locales/fr.json index 7e148bb4b4..1386bd8d96 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -257,22 +257,30 @@ "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Est-il en cours d'exécution ? Vérifiez également votre connectivité réseau." }, "orgPost": { - "title": "Postes de Talawa", - "searchPost": "Rechercher un article", - "posts": "Des postes", - "createPostnot found": "Créer un article", + "title": "Talawa Publications", + "searchPost": "Rechercher une publication", + "posts": "Publications", + "createPost": "Créer une publication", "postDetails": "Détails de la publication", + "postTitle1": "Écrire le titre de la publication", "postTitle": "Titre", "information": "Informations", - "image": "Image", - "video": "Vidéo", - "addPost": "Ajouter un article", + "information1": "Écrire les informations de la publication", + "image": "Image de la publication", + "video": "Vidéo de la publication", + "addPost": "Ajouter une publication", "searchTitle": "Rechercher par titre", "searchText": "Rechercher par texte", - "ptitle": "Titre de l'article", - "postDes": "De quoi parlez-vous ?", + "ptitle": "Titre de la publication", + "postDes": "De quoi voulez-vous parler?", "Title": "Titre", - "Text": "Texte" + "Text": "Texte", + "cancel": "Annuler", + "searchBy": "Rechercher par", + "Oldest": "Les plus anciennes d'abord", + "Latest": "Les plus récentes d'abord", + "sortPost": "Trier les publications", + "tag": "Votre navigateur ne prend pas en charge la balise vidéo" }, "postNotFound": { "post": "Poste", @@ -294,19 +302,22 @@ "author": "Auteur", "imageURL": "URL de l'image", "videoURL": "URL de la vidéo", - "deletePost": "Supprimer le message", - "deletePostMsg": "Voulez-vous supprimer ce message ?", + "edit": "Modifier la publication", + "deletePost": "Supprimer la publication", + "deletePostMsg": "Voulez-vous supprimer cette publication ?", "no": "Non", "yes": "Oui", - "editPost": "Modifier le message", + "editPost": "Modifier la publication", "postTitle": "Titre", "information": "Informations", "image": "Image", "video": "Vidéo", - "close": "Proche", - "updatePost": "Mettre à jour le message", - "postDeleted": "Message supprimé avec succès.", - "postUpdated": "Message mis à jour avec succès.", + "close": "Fermer", + "pin": "Épingler la publication", + "updatePost": "Mettre à jour la publication", + "postDeleted": "Publication supprimée avec succès.", + "postUpdated": "Publication mise à jour avec succès.", + "tag": "Votre navigateur ne prend pas en charge la balise vidéo", "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Est-il en cours d'exécution ? Vérifiez également votre connectivité réseau." }, "blockUnblockUser": { diff --git a/public/locales/hi.json b/public/locales/hi.json index 48a97211f8..f7091382ab 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -257,22 +257,30 @@ "talawaApiUnavailable": "तलवा-एपीआई सेवा उपलब्ध नहीं है। क्या यह चल रहा है? अपनी नेटवर्क कनेक्टिविटी भी जांचें।" }, "orgPost": { - "title": "तलावा पोस्ट", - "searchPost": "खोज पोस्ट", - "posts": "पोस्ट", - "createPost": "पोस्ट बनाएं", + "title": "तलवा पोस्ट्स", + "searchPost": "पोस्ट खोजें", + "posts": "पोस्ट्स", + "createPost": "पोस्ट बनाएँ", "postDetails": "पोस्ट विवरण", + "postTitle1": "पोस्ट का शीर्षक लिखें", "postTitle": "शीर्षक", "information": "जानकारी", - "image": "छवि", - "video": "वीडियो", + "information1": "पोस्ट की जानकारी लिखें", + "image": "पोस्ट छवि", + "video": "पोस्ट वीडियो", "addPost": "पोस्ट जोड़ें", "searchTitle": "शीर्षक से खोजें", - "searchText": "पाठ द्वारा खोजें", - "ptitle": "शीर्षक पोस्ट करें", - "postDes": "आपको किस बारे में बात करनी है?", + "searchText": "टेक्स्ट से खोजें", + "ptitle": "पोस्ट का शीर्षक", + "postDes": "आप किस बारे में बात करना चाहते हैं?", "Title": "शीर्षक", - "Text": "मूलपाठ" + "Text": "टेक्स्ट", + "cancel": "रद्द करें", + "searchBy": "इसके द्वारा खोजें", + "Oldest": "सबसे पुराना पहले", + "Latest": "सबसे नवीनतम पहले", + "sortPost": "पोस्ट को क्रमित करें", + "tag": "आपका ब्राउज़र वीडियो टैग का समर्थन नहीं करता" }, "postNotFound": { "post": "पोस्ट", @@ -292,22 +300,25 @@ }, "orgPostCard": { "author": "लेखक", - "imageURL": "छवि यूआरएल", - "videoURL": "वीडियो यूआरएल", - "deletePost": "पोस्ट को हटाएं", + "imageURL": "छवि URL", + "videoURL": "वीडियो URL", + "edit": "पोस्ट संपादित करें", + "deletePost": "पोस्ट हटाएं", "deletePostMsg": "क्या आप इस पोस्ट को हटाना चाहते हैं?", "no": "नहीं", "yes": "हाँ", - "editPost": "संपादित पोस्ट", + "editPost": "पोस्ट संपादित करें", "postTitle": "शीर्षक", "information": "जानकारी", "image": "छवि", "video": "वीडियो", - "close": "बंद करना", - "updatePost": "अपडेट पोस्ट", - "postDeleted": "पोस्ट सफलतापूर्वक हटाई गई।", - "postUpdated": "पोस्ट सफलतापूर्वक अपडेट किया गया।", - "talawaApiUnavailable": "तलवा-एपीआई सेवा उपलब्ध नहीं है। क्या यह चल रहा है? अपनी नेटवर्क कनेक्टिविटी भी जांचें।" + "close": "बंद करें", + "updatePost": "पोस्ट अपडेट करें", + "postDeleted": "पोस्ट सफलतापूर्वक हटा दी गई है।", + "pin": "पोस्ट पिन करें", + "postUpdated": "पोस्ट सफलतापूर्वक अपडेट की गई है।", + "tag": "आपका ब्राउज़र वीडियो टैग का समर्थन नहीं करता", + "talawaApiUnavailable": "Talawa-API सेवा उपलब्ध नहीं है। क्या यह चल रहा है? अपनी नेटवर्क कनेक्टिविटी भी जाँचें।" }, "blockUnblockUser": { "title": "तलावा ब्लॉक/अनब्लॉक यूजर", diff --git a/public/locales/sp.json b/public/locales/sp.json index dfcb6165e5..dd53814d12 100644 --- a/public/locales/sp.json +++ b/public/locales/sp.json @@ -257,22 +257,30 @@ "talawaApiUnavailable": "El servicio Talawa-API no está disponible. ¿Está funcionando? Compruebe también la conectividad de su red." }, "orgPost": { - "title": "Puestos Talawa", - "searchPost": "Buscar publicación", + "title": "Publicaciones de Talawa", + "searchPost": "Buscar Publicación", "posts": "Publicaciones", - "createPost": "Crear publicación", - "postDetails": "Detalles de la publicación", + "createPost": "Crear Publicación", + "postDetails": "Detalles de la Publicación", + "postTitle1": "Escribir título de la publicación", "postTitle": "Título", "information": "Información", - "image": "Imagen", - "video": "Video", - "addPost": "Añadir publicación", - "searchTitle": "Buscar por título", - "searchText": "Buscar por texto", - "ptitle": "Título de la entrada", - "postDes": "¿De qué hablar?", + "information1": "Escribir información de la publicación", + "image": "Imagen de la Publicación", + "video": "Video de la Publicación", + "addPost": "Agregar Publicación", + "searchTitle": "Buscar por Título", + "searchText": "Buscar por Texto", + "ptitle": "Título de la Publicación", + "postDes": "¿De qué quieres hablar?", "Title": "Título", - "Text": "Texto" + "Text": "Texto", + "cancel": "Cancelar", + "searchBy": "Buscar por", + "Oldest": "Más Antiguas Primero", + "Latest": "Más Recientes Primero", + "sortPost": "Ordenar Publicaciones", + "tag": "Su navegador no admite la etiqueta de video" }, "postNotFound": { "post": "Publicaciones", @@ -290,24 +298,27 @@ "admin not found!": "Administrador no encontrado!", "roles not found!": "roles no encontrados!" }, + "orgPostCard": { "author": "Autor", - "imageURL": "URL de la imagen", - "videoURL": "URL del vídeo", - "deletePost": "Eliminar mensaje", - "deletePostMsg": "¿Quieres eliminar esta publicación?", + "imageURL": "URL de la Imagen", + "videoURL": "URL del Video", + "edit": "Editar Publicación", + "deletePost": "Eliminar Publicación", + "deletePostMsg": "¿Desea eliminar esta publicación?", "no": "No", "yes": "Sí", - "editPost": "Editar post", + "editPost": "Editar Publicación", "postTitle": "Título", "information": "Información", "image": "Imagen", "video": "Video", - "close": "Cerca", - "updatePost": "Actualizar publicación", - "postDeleted": "Publicación eliminada con éxito.", - "postUpdated": "Publicación actualizada con éxito.", - "talawaApiUnavailable": "El servicio Talawa-API no está disponible. ¿Está funcionando? Compruebe también la conectividad de su red." + "close": "Cerrar", + "updatePost": "Actualizar Publicación", + "postDeleted": "Publicación eliminada exitosamente.", + "postUpdated": "Publicación actualizada exitosamente.", + "tag": "Su navegador no admite la etiqueta de video", + "talawaApiUnavailable": "El servicio Talawa-API no está disponible. ¿Está en funcionamiento? Compruebe también su conectividad de red." }, "blockUnblockUser": { "title": "Usuario de bloqueo/desbloqueo de Talawa", diff --git a/public/locales/zh.json b/public/locales/zh.json index d55a606326..dbb937c94e 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -257,22 +257,30 @@ "talawaApiUnavailable": "服務不可用。它在運行嗎?還要檢查您的網絡連接。" }, "orgPost": { - "title": "塔拉瓦郵報", + "title": "塔拉瓦帖子", "searchPost": "搜索帖子", "posts": "帖子", - "createPost": "創建帖子", - "postDetails": "發布詳細信息", - "postTitle": "標題", + "createPost": "创建帖子", + "postDetails": "帖子详情", + "postTitle1": "填写帖子标题", + "postTitle": "标题", "information": "信息", - "image": "圖片", - "video": "視頻", + "information1": "填写帖子信息", + "image": "帖子图片", + "video": "帖子视频", "addPost": "添加帖子", - "searchTitle": "按標題搜索", + "searchTitle": "按标题搜索", "searchText": "按文本搜索", - "ptitle": "帖子標題", - "postDes": "你要談什麼?", - "Title": "標題", - "Text": "文本" + "ptitle": "帖子标题", + "postDes": "您想讨论什么?", + "Title": "标题", + "Text": "文本", + "cancel": "取消", + "searchBy": "按方式搜索", + "Oldest": "最旧的优先", + "Latest": "最新的优先", + "sortPost": "排序帖子", + "tag": "您的浏览器不支持视频标签" }, "postNotFound": { "post": "郵政", @@ -292,22 +300,25 @@ }, "orgPostCard": { "author": "作者", - "imageURL": "圖片網址", - "videoURL": "視頻網址", - "deletePost": "刪除帖子", - "deletePostMsg": "你想刪除這個帖子嗎?", - "no": "不", - "yes": "是的", - "editPost": "編輯帖子", - "postTitle": "標題", + "imageURL": "图像链接", + "videoURL": "视频链接", + "edit": "编辑帖子", + "deletePost": "删除帖子", + "deletePostMsg": "您确定要删除此帖子吗?", + "no": "否", + "yes": "是", + "editPost": "编辑帖子", + "postTitle": "标题", "information": "信息", - "image": "圖片", - "video": "視頻", - "close": "關", + "image": "图片", + "video": "视频", + "close": "关闭", + "pin": "置顶帖子", "updatePost": "更新帖子", - "postDeleted": "帖子刪除成功。", - "postUpdated": "帖子更新成功。", - "talawaApiUnavailable": "服務不可用。它在運行嗎?還要檢查您的網絡連接。" + "postDeleted": "帖子成功删除。", + "postUpdated": "帖子成功更新。", + "tag": "您的浏览器不支持视频标签", + "talawaApiUnavailable": "Talawa-API 服务不可用。是否正在运行?还请检查您的网络连接。" }, "blockUnblockUser": { "title": "塔拉瓦封鎖/解除封鎖用戶", diff --git a/src/GraphQl/Mutations/mutations.ts b/src/GraphQl/Mutations/mutations.ts index 7090bd10d7..fb303cb840 100644 --- a/src/GraphQl/Mutations/mutations.ts +++ b/src/GraphQl/Mutations/mutations.ts @@ -402,8 +402,22 @@ export const ADD_PLUGIN_MUTATION = gql` `; export const UPDATE_POST_MUTATION = gql` - mutation UpdatePost($id: ID!, $title: String, $text: String) { - updatePost(id: $id, data: { title: $title, text: $text }) { + mutation UpdatePost( + $id: ID! + $title: String + $text: String + $imageUrl: String + $videoUrl: String + ) { + updatePost( + id: $id + data: { + title: $title + text: $text + imageUrl: $imageUrl + videoUrl: $videoUrl + } + ) { _id } } @@ -661,3 +675,10 @@ export const PLUGIN_SUBSCRIPTION = gql` } } `; +export const TOGGLE_PINNED_POST = gql` + mutation TogglePostPin($id: ID!) { + togglePostPin(id: $id) { + _id + } + } +`; diff --git a/src/GraphQl/Queries/Queries.ts b/src/GraphQl/Queries/Queries.ts index 13dfc9c135..fccad7d88e 100644 --- a/src/GraphQl/Queries/Queries.ts +++ b/src/GraphQl/Queries/Queries.ts @@ -594,6 +594,7 @@ export const ORGANIZATION_POST_CONNECTION_LIST = gql` lastName email } + pinned likeCount commentCount comments { @@ -610,6 +611,7 @@ export const ORGANIZATION_POST_CONNECTION_LIST = gql` } text } + createdAt likedBy { _id firstName diff --git a/src/components/OrgPostCard/OrgPostCard.module.css b/src/components/OrgPostCard/OrgPostCard.module.css index 73cf9c4f46..c7ff8073d2 100644 --- a/src/components/OrgPostCard/OrgPostCard.module.css +++ b/src/components/OrgPostCard/OrgPostCard.module.css @@ -4,12 +4,162 @@ .cards > h3 { font-size: 17px; } +.card { + width: 100%; + height: 20rem; + margin-bottom: 2rem; +} .postimage { - border-radius: 6px; + border-radius: 0px; + width: 100%; + height: 12rem; + max-width: 100%; + max-height: 12rem; + object-fit: cover; + position: relative; + color: black; +} +.preview { + display: flex; + position: relative; + width: 100%; + margin-top: 10px; + justify-content: center; +} +.preview img { + width: 400px; + height: auto; +} +.preview video { + width: 400px; + height: auto; +} +.nopostimage { + border-radius: 0px; + width: 100%; + height: 12rem; + max-height: 12rem; + object-fit: cover; + position: relative; +} +.cards:hover { + filter: brightness(0.8); +} +.cards:hover::before { + opacity: 0.5; +} +.knowMoreText { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + opacity: 0; + color: white; + padding: 10px; + font-weight: bold; + font-size: 1.5rem; + transition: opacity 0.3s ease-in-out; +} + +.cards:hover .knowMoreText { + opacity: 1; +} +.modal { + position: fixed; + top: 0; + left: 0; width: 100%; - max-width: 240px; - height: 150px; - max-height: 250px; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba( + 0, + 0, + 0, + 0.9 + ); /* Dark grey modal background with transparency */ + z-index: 9999; +} + +.modalContent { + display: flex; + align-items: center; + justify-content: center; + background-color: #fff; + padding: 20px; + max-width: 800px; + max-height: 600px; + overflow: auto; +} + +.modalImage { + flex: 1; + margin-right: 20px; + width: 25rem; + height: 15rem; +} +.nomodalImage { + flex: 1; + margin-right: 20px; + width: 100%; + height: 15rem; +} + +.modalImage img, +.modalImage video { + border-radius: 0px; + width: 100%; + height: 25rem; + max-width: 25rem; + max-height: 15rem; + object-fit: cover; + position: relative; +} +.modalInfo { + flex: 1; +} +.title { + font-size: 16px; + color: #000; + font-weight: 600; +} +.text { + font-size: 13px; + color: #000; + font-weight: 300; +} +.author { + color: #737373; + font-weight: 100; + font-size: 13px; +} +.closeButton { + position: relative; + bottom: 5rem; + right: 10px; + padding: 4px; + background-color: red; /* Red close button color */ + color: #fff; + border: none; + cursor: pointer; +} +.closeButtonP { + position: absolute; + top: 0px; + right: 0px; + background: transparent; + transform: scale(1.2); + cursor: pointer; + border: none; + color: #707070; + font-weight: 600; + font-size: 16px; + cursor: pointer; +} +.cards:hover::after { + opacity: 1; + mix-blend-mode: normal; } .cards > p { font-size: 14px; @@ -25,6 +175,9 @@ } .infodiv { margin-bottom: 7px; + width: 15rem; + text-align: justify; + word-wrap: break-word; } .infodiv > p { margin: 0; @@ -40,8 +193,7 @@ transform: scale(0.75); cursor: pointer; } - -.cards { +/* .cards { width: 75%; background: #fcfcfc; margin: 10px 40px; @@ -52,16 +204,14 @@ margin-right: 30px; color: #737373; box-sizing: border-box; -} +} */ .cards:last-child:nth-last-child(odd) { grid-column: auto / span 2; } - .cards:first-child:nth-last-child(even), .cards:first-child:nth-last-child(even) ~ .box { grid-column: auto / span 1; } - .toggleClickBtn { color: #31bb6b; cursor: pointer; @@ -69,7 +219,60 @@ font-size: 12px; background-color: white; } - .toggleClickBtnNone { display: none; } +/* Menu Modal Styles */ +.menuModal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.7); /* Dark grey modal background */ + z-index: 9999; +} + +.menuContent { + display: flex; + align-items: center; + justify-content: center; + background-color: #fff; + padding-top: 20px; + max-width: 700px; + max-height: 500px; + overflow: hidden; + position: relative; +} + +.menuOptions { + list-style-type: none; + padding: 0; + margin: 0; +} + +.menuOptions li { + padding: 10px; + border-bottom: 1px solid #ccc; + padding-left: 100px; + padding-right: 100px; + cursor: pointer; +} + +.moreOptionsButton { + position: relative; + bottom: 5rem; + right: 10px; + padding: 2px; + background-color: transparent; + color: #000; + border: none; + cursor: pointer; +} +.list { + color: red; + cursor: pointer; +} diff --git a/src/components/OrgPostCard/OrgPostCard.test.tsx b/src/components/OrgPostCard/OrgPostCard.test.tsx index dc8a7e5fd4..e2a209c00f 100644 --- a/src/components/OrgPostCard/OrgPostCard.test.tsx +++ b/src/components/OrgPostCard/OrgPostCard.test.tsx @@ -1,19 +1,25 @@ import React from 'react'; -import { act, render, screen, fireEvent } from '@testing-library/react'; +import { + act, + render, + screen, + fireEvent, + waitFor, +} from '@testing-library/react'; import { MockedProvider } from '@apollo/react-testing'; -import userEvent from '@testing-library/user-event'; -import { I18nextProvider } from 'react-i18next'; - import OrgPostCard from './OrgPostCard'; +import { I18nextProvider } from 'react-i18next'; +import userEvent from '@testing-library/user-event'; +import 'jest-localstorage-mock'; import { DELETE_POST_MUTATION, UPDATE_POST_MUTATION, + TOGGLE_PINNED_POST, } from 'GraphQl/Mutations/mutations'; import i18nForTest from 'utils/i18nForTest'; import { StaticMockLink } from 'utils/StaticMockLink'; - -import mockedStyles from './OrgPostCard.module.css'; - +import convertToBase64 from 'utils/convertToBase64'; +import { BrowserRouter } from 'react-router-dom'; const MOCKS = [ { request: { @@ -23,7 +29,7 @@ const MOCKS = [ result: { data: { removePost: { - _id: '1', + _id: '123', }, }, }, @@ -45,7 +51,29 @@ const MOCKS = [ }, }, }, + { + request: { + query: TOGGLE_PINNED_POST, + variables: { + id: '32', + }, + }, + result: { + data: { + togglePostPin: { + _id: '32', + }, + }, + }, + }, ]; +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); const link = new StaticMockLink(MOCKS, true); async function wait(ms = 100): Promise { await act(() => { @@ -54,7 +82,6 @@ async function wait(ms = 100): Promise { }); }); } - describe('Testing Organization Post Card', () => { const props = { key: '123', @@ -62,37 +89,73 @@ describe('Testing Organization Post Card', () => { postTitle: 'Event Info', postInfo: 'Time change', postAuthor: 'John Doe', - postPhoto: 'photoLink', - postVideo: 'videoLink', + postPhoto: 'test.png', + postVideo: 'test.mp4', + pinned: false, }; - + jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, + })); + jest.mock('react', () => ({ + ...jest.requireActual('react'), + useRef: jest.fn(), + })); global.alert = jest.fn(); - test('should render props and text elements test for the page component', async () => { - global.confirm = (): boolean => true; + test('renders with default props', () => { + const { getByAltText, getByTestId } = render( + + + + + + ); + expect(getByTestId('card-text')).toBeInTheDocument(); + expect(getByTestId('card-title')).toBeInTheDocument(); + expect(getByAltText('image')).toBeInTheDocument(); + }); - render( + test('toggles "Read more" button', () => { + const { getByTestId } = render( ); + userEvent.click(screen.getByAltText('image')); + const toggleButton = getByTestId('toggleBtn'); + fireEvent.click(toggleButton); + expect(toggleButton).toHaveTextContent('hide'); + fireEvent.click(toggleButton); + expect(toggleButton).toHaveTextContent('Read more'); + }); + test('opens and closes edit modal', async () => { + localStorage.setItem('id', '123'); + render( + + + + + + ); await wait(); + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('moreiconbtn')); + userEvent.click(screen.getByTestId('editPostModalBtn')); - expect(screen.getByText(/Author:/i)).toBeInTheDocument(); - expect(screen.getByText(/Video URL:/i)).toBeInTheDocument(); - expect(screen.getByText(props.postTitle)).toBeInTheDocument(); - expect(screen.getByText(props.postInfo)).toBeInTheDocument(); - expect(screen.getByText(props.postAuthor)).toBeInTheDocument(); - expect(screen.getByAltText(/image not found/i)).toBeInTheDocument(); - expect(screen.getByText(props.postVideo)).toBeInTheDocument(); + const createOrgBtn = screen.getByTestId('modalOrganizationHeader'); + expect(createOrgBtn).toBeInTheDocument(); + userEvent.click(createOrgBtn); + userEvent.click(screen.getByTestId('closeOrganizationModal')); }); - test('Should render text elements when props value is not passed', async () => { global.confirm = (): boolean => false; - render( @@ -100,19 +163,11 @@ describe('Testing Organization Post Card', () => { ); - await wait(); - - expect(screen.getByText(/Author:/i)).toBeInTheDocument(); - expect(screen.getByText(/Video URL:/i)).toBeInTheDocument(); - expect(screen.getByText(props.postTitle)).toBeInTheDocument(); - expect(screen.getByText(props.postInfo)).toBeInTheDocument(); - expect(screen.getByText(props.postAuthor)).toBeInTheDocument(); - expect(screen.getByAltText(/image not found/i)).toBeInTheDocument(); - expect(screen.getByText(props.postVideo)).toBeInTheDocument(); + userEvent.click(screen.getByAltText('image')); + expect(screen.getByAltText('Post Image')).toBeInTheDocument(); }); - - test('Testing post update functionality', async () => { + test('Testing post updating after post is updated', async () => { render( @@ -123,14 +178,15 @@ describe('Testing Organization Post Card', () => { await wait(); - userEvent.click(screen.getByTestId('editPostModalBtn')); + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('moreiconbtn')); + userEvent.click(screen.getByTestId('editPostModalBtn')); userEvent.type(screen.getByTestId('updateTitle'), 'updated title'); userEvent.type(screen.getByTestId('updateText'), 'This is a updated text'); userEvent.click(screen.getByTestId('updatePostBtn')); }); - - test('Testing delete post funcationality', async () => { + test('Testing pin post functionality', async () => { render( @@ -141,11 +197,31 @@ describe('Testing Organization Post Card', () => { await wait(); - userEvent.click(screen.getByTestId('deletePostModalBtn')); - userEvent.click(screen.getByTestId(/deletePostBtn/i)); + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('moreiconbtn')); + + userEvent.click(screen.getByTestId('pinpostBtn')); }); + test('Testing post delete functionality', async () => { + render( + + + + + + + + ); - test('should toggle post visibility when button is clicked', () => { + await wait(); + + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('moreiconbtn')); + + userEvent.click(screen.getByTestId('deletePostModalBtn')); + fireEvent.click(screen.getByTestId('deletePostBtn')); + }); + test('Testing close functionality of primary modal', async () => { render( @@ -154,30 +230,37 @@ describe('Testing Organization Post Card', () => { ); - const toggleButton = screen.getByTestId('toggleBtn'); - - expect(screen.getByText('Read more')).toBeInTheDocument(); - - fireEvent.click(toggleButton); + await wait(); - expect(screen.getByText('hide')).toBeInTheDocument(); + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('closeiconbtn')); + }); + test('Testing close functionality of secondary modal', async () => { + render( + + + + + + ); - fireEvent.click(toggleButton); + await wait(); - expect(screen.getByText('Read more')).toBeInTheDocument(); + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('moreiconbtn')); + userEvent.click(screen.getByTestId('closebtn')); }); - - test('should toggle post content', () => { + test('renders without "Read more" button when postInfo length is less than or equal to 43', () => { const props = { key: '123', id: '12', postTitle: 'Event Info', - postInfo: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + postInfo: 'Lorem ipsum dolor sit amet', postAuthor: 'John Doe', postPhoto: 'photoLink', postVideo: 'videoLink', + pinned: false, }; - render( @@ -185,34 +268,100 @@ describe('Testing Organization Post Card', () => { ); + }); + test('updates state variables correctly when handleEditModal is called', () => { + const link2 = new StaticMockLink(MOCKS, true); + render( + + + + ); + userEvent.click(screen.getByAltText('image')); + + userEvent.click(screen.getByTestId('moreiconbtn')); + + expect(screen.queryByTestId('editPostModalBtn')).toBeInTheDocument(); + + userEvent.click(screen.getByTestId('editPostModalBtn')); + + // expect(screen.queryByTestId('editPostModalBtn')).toBeInTheDocument(); + // expect(screen.queryByTestId('deletePostModalBtn')).not.toBeInTheDocument(); + // expect(screen.queryByTestId('closeiconbtn')).not.toBeInTheDocument(); - const toggleBtn = screen.getByRole('toggleBtn'); + // expect(screen.getByTestId('editPostModal')).toHaveClass('show'); + // expect(screen.getByTestId('deletePostModal')).not.toHaveClass('show'); - expect( - screen.getByText('Lorem ipsum dolor sit amet, consectetur ...') - ).toBeInTheDocument(); - expect(toggleBtn).toHaveTextContent('Read more'); - expect(toggleBtn).toHaveClass(mockedStyles.toggleClickBtn); + // expect(screen.getByTestId('modalVisible')).toBe('false'); + // expect(screen.getByTestId('menuVisible')).toBe('false'); + // expect(screen.getByTestId('showEditModal')).toBe('true'); + // expect(screen.getByTestId('showDeleteModal')).toBe('false'); + }); + test('clears postvideo state and resets file input value', async () => { + const { getByTestId } = render( + + + + + + ); - fireEvent.click(toggleBtn); + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('moreiconbtn')); - expect(screen.getByTestId('toggleContent').innerHTML).toEqual( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + userEvent.click(screen.getByTestId('editPostModalBtn')); + userEvent.click(screen.getByTestId('closePreview')); + + fireEvent.change(getByTestId('postVideoUrl'), { + target: { value: '' }, + }); + userEvent.click(screen.getByPlaceholderText(/video/i)); + const input = getByTestId('postVideoUrl'); + const file = new File(['test-video'], 'test.mp4', { type: 'video/mp4' }); + Object.defineProperty(input, 'files', { + value: [file], + }); + fireEvent.change(input); + await waitFor(() => { + convertToBase64(file); + }); + }); + test('clears postimage state and resets file input value', async () => { + const { getByTestId } = render( + + + + + ); - expect(toggleBtn).toHaveTextContent('hide'); - expect(toggleBtn).toHaveClass(mockedStyles.toggleClickBtn); + + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('moreiconbtn')); + + userEvent.click(screen.getByTestId('editPostModalBtn')); + userEvent.click(screen.getByTestId('closePreview')); + + fireEvent.change(getByTestId('postImageUrl'), { + target: { value: '' }, + }); + userEvent.click(screen.getByPlaceholderText(/image/i)); + const input = getByTestId('postImageUrl'); + const file = new File(['test-image'], 'test.jpg', { type: 'image/jpeg' }); + Object.defineProperty(input, 'files', { + value: [file], + }); + fireEvent.change(input); + + // Simulate the asynchronous base64 conversion function + await waitFor(() => { + convertToBase64(file); // Replace with the expected base64-encoded image + }); + document.getElementById = jest.fn(() => input); + const clearImageButton = getByTestId('closeimage'); + fireEvent.click(clearImageButton); }); + test('Testing create organization modal', async () => { + localStorage.setItem('id', '123'); - test('renders without "Read more" button when postInfo length is less than or equal to 43', () => { - const props = { - key: '123', - id: '12', - postTitle: 'Event Info', - postInfo: 'Lorem ipsum dolor sit amet', - postAuthor: 'John Doe', - postPhoto: 'photoLink', - postVideo: 'videoLink', - }; render( @@ -220,5 +369,35 @@ describe('Testing Organization Post Card', () => { ); + + await wait(); + userEvent.click(screen.getByAltText('image')); + + userEvent.click(screen.getByTestId('moreiconbtn')); + + userEvent.click(screen.getByTestId('editPostModalBtn')); + const createOrgBtn = screen.getByTestId('modalOrganizationHeader'); + expect(createOrgBtn).toBeInTheDocument(); + userEvent.click(createOrgBtn); + userEvent.click(screen.getByTestId('closeOrganizationModal')); + }); + test('should toggle post pin when pin button is clicked', async () => { + const { getByTestId } = render( + + + + + + ); + userEvent.click(screen.getByAltText('image')); + + userEvent.click(screen.getByTestId('moreiconbtn')); + const pinButton = getByTestId('pinpostBtn'); + fireEvent.click(pinButton); + await waitFor(() => { + expect(MOCKS[2].request.variables).toEqual({ + id: '32', + }); + }); }); }); diff --git a/src/components/OrgPostCard/OrgPostCard.tsx b/src/components/OrgPostCard/OrgPostCard.tsx index 3eb2d675f2..5845ef90ba 100644 --- a/src/components/OrgPostCard/OrgPostCard.tsx +++ b/src/components/OrgPostCard/OrgPostCard.tsx @@ -1,19 +1,24 @@ import type { ChangeEvent } from 'react'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { useMutation } from '@apollo/client'; import Button from 'react-bootstrap/Button'; import Modal from 'react-bootstrap/Modal'; +import Card from 'react-bootstrap/Card'; import { toast } from 'react-toastify'; - +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import CloseIcon from '@mui/icons-material/Close'; +import PushPinIcon from '@mui/icons-material/PushPin'; +import AboutImg from 'assets/images/defaultImg.png'; import { DELETE_POST_MUTATION, UPDATE_POST_MUTATION, + TOGGLE_PINNED_POST, } from 'GraphQl/Mutations/mutations'; -import defaultImg from 'assets/images/blank.png'; import { useTranslation } from 'react-i18next'; import { errorHandler } from 'utils/errorHandler'; import styles from './OrgPostCard.module.css'; import { Form } from 'react-bootstrap'; +import convertToBase64 from 'utils/convertToBase64'; interface InterfaceOrgPostCardProps { key: string; @@ -23,22 +28,103 @@ interface InterfaceOrgPostCardProps { postAuthor: string; postPhoto: string; postVideo: string; + pinned: boolean; } -function orgPostCard(props: InterfaceOrgPostCardProps): JSX.Element { +// eslint-disable-next-line @typescript-eslint/naming-convention +export default function OrgPostCard( + props: InterfaceOrgPostCardProps +): JSX.Element { const [postformState, setPostFormState] = useState({ posttitle: '', postinfo: '', + postphoto: '', + postvideo: '', + pinned: false, }); const [togglePost, setPostToggle] = useState('Read more'); const [showEditModal, setShowEditModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [menuVisible, setMenuVisible] = useState(false); + const [playing, setPlaying] = useState(false); + const videoRef = useRef(null); + const [toggle] = useMutation(TOGGLE_PINNED_POST); + const togglePostPin = async (id: string, pinned: boolean): Promise => { + try { + const { data } = await toggle({ + variables: { + id, + }, + }); + if (data) { + setModalVisible(false); + setMenuVisible(false); + toast.success(`${pinned ? 'Post unpinned' : 'Post pinned'}`); + setTimeout(() => { + window.location.reload(); + }, 2000); + } + } catch (error: any) { + console.log(error); + /* istanbul ignore next */ + errorHandler(t, error); + } + }; + const toggleShowEditModal = (): void => { + setPostFormState({ + posttitle: props.postTitle, + postinfo: props.postInfo, + postphoto: props.postPhoto, + postvideo: props.postVideo, + pinned: props.pinned, + }); + setShowEditModal((prev) => !prev); + }; + const toggleShowDeleteModal = (): void => setShowDeleteModal((prev) => !prev); + + const handleVideoPlay = (): void => { + setPlaying(true); + videoRef.current?.play(); + }; + + const handleVideoPause = (): void => { + setPlaying(false); + videoRef.current?.pause(); + }; + const handleCardClick = (): void => { + setModalVisible(true); + }; - const toggleShowEditModal = (): void => setShowEditModal(!showEditModal); - const toggleShowDeleteModal = (): void => - setShowDeleteModal(!showDeleteModal); + const handleMoreOptionsClick = (): void => { + setMenuVisible(true); + }; + const clearImageInput = (): void => { + setPostFormState({ + ...postformState, + postphoto: '', + }); + const fileInput = document.getElementById( + 'postImageUrl' + ) as HTMLInputElement; + if (fileInput) { + fileInput.value = ''; + } + }; + const clearVideoInput = (): void => { + setPostFormState({ + ...postformState, + postvideo: '', + }); + const fileInput = document.getElementById( + 'postVideoUrl' + ) as HTMLInputElement; + if (fileInput) { + fileInput.value = ''; + } + }; function handletoggleClick(): void { if (togglePost === 'Read more') { setPostToggle('hide'); @@ -47,10 +133,30 @@ function orgPostCard(props: InterfaceOrgPostCardProps): JSX.Element { } } + function handleEditModal(): void { + setModalVisible(false); + setMenuVisible(false); + setShowEditModal(true); + setPostFormState({ + ...postformState, + postphoto: props.postPhoto, + postvideo: props.postVideo, + }); + } + + function handleDeleteModal(): void { + setModalVisible(false); + setMenuVisible(false); + setShowDeleteModal(true); + } + useEffect(() => { setPostFormState({ posttitle: props.postTitle, postinfo: props.postInfo, + postphoto: props.postPhoto, + postvideo: props.postVideo, + pinned: props.pinned, }); }, []); @@ -58,36 +164,35 @@ function orgPostCard(props: InterfaceOrgPostCardProps): JSX.Element { keyPrefix: 'orgPostCard', }); - const [create] = useMutation(DELETE_POST_MUTATION); - const [updatePost] = useMutation(UPDATE_POST_MUTATION); + const [deletePostMutation] = useMutation(DELETE_POST_MUTATION); + const [updatePostMutation] = useMutation(UPDATE_POST_MUTATION); const deletePost = async (): Promise => { try { - const { data } = await create({ + const { data } = await deletePostMutation({ variables: { id: props.id, }, }); - /* istanbul ignore next */ if (data) { toast.success(t('postDeleted')); setTimeout(() => { window.location.reload(); - }, 2000); + }); } } catch (error: any) { - /* istanbul ignore next */ errorHandler(t, error); } }; - const handleInputEvent = ( e: ChangeEvent ): void => { const { name, value } = e.target; - - setPostFormState({ ...postformState, [name]: value }); + setPostFormState((prevPostFormState) => ({ + ...prevPostFormState, + [name]: value, + })); }; const updatePostHandler = async ( @@ -96,15 +201,27 @@ function orgPostCard(props: InterfaceOrgPostCardProps): JSX.Element { e.preventDefault(); try { - const { data } = await updatePost({ + let imageUrl = null; + let videoUrl = null; + + if (e.target?.postphoto && e.target?.postphoto.files.length > 0) { + imageUrl = postformState.postphoto; + } + + if (e.target?.postvideo && e.target?.postvideo.files.length > 0) { + videoUrl = postformState.postvideo; + } + + const { data } = await updatePostMutation({ variables: { id: props.id, title: postformState.posttitle, text: postformState.postinfo, + imageUrl, + videoUrl, }, }); - /* istanbul ignore next */ if (data) { toast.success(t('postUpdated')); setTimeout(() => { @@ -112,90 +229,221 @@ function orgPostCard(props: InterfaceOrgPostCardProps): JSX.Element { }, 2000); } } catch (error: any) { - /* istanbul ignore next */ toast.error(error.message); } }; return ( <> -
-
-
-

{props.postTitle}

-
+
+
+ {props.postVideo && ( + + + + {props.pinned && ( + + )} + + {props.postTitle} + + + {props.postInfo} + + + {props.postAuthor} + + + + )} {props.postPhoto ? ( -

- - {' '} - + + + {props.pinned && ( + + )} + + {props.postTitle} + + {props.postInfo} + {props.postAuthor} + + + ) : !props.postVideo ? ( + + + - -

+ + {props.pinned && ( + + )} + + {props.postTitle} + + + {props.postInfo && props.postInfo.length > 20 + ? props.postInfo.substring(0, 20) + '...' + : props.postInfo} + {' '} + + {props.postAuthor} + + + + ) : ( - image not found + '' )} -

- {t('author')}: {props.postAuthor} -

-
- {togglePost === 'Read more' ? ( -

- {props.postInfo.length > 43 - ? props.postInfo.substring(0, 40) + '...' - : props.postInfo} -

- ) : ( -

{props.postInfo}

- )} - +
+ {modalVisible && ( +
+
+ {props.postPhoto && ( +
+ Post Image +
+ )} + {props.postVideo && ( +
+ +
+ )} + {!props.postPhoto && !props.postVideo && ( +
+ {' '} + Post Image +
+ )} + +
+

+ {t('author')}: {props.postAuthor} +

+
+ {togglePost === 'Read more' ? ( +

+ {props.postInfo.length > 43 + ? props.postInfo.substring(0, 40) + '...' + : props.postInfo} +

+ ) : ( +

{props.postInfo}

+ )} + +
+
+ + +
-

- {t('videoURL')}: - - {' '} - {props.postVideo} - -

+ )} -
- - + {menuVisible && ( +
+
+
    +
  • + {t('edit')} +
  • +
  • + {t('deletePost')} +
  • +
  • => + togglePostPin(props.id, props.pinned) + } + > + {!props.pinned ? 'Pin post' : 'Unpin post'} +
  • +
  • setMenuVisible(false)} + data-testid="closebtn" + > + {t('close')} +
  • +
+
-
+ )}
{/* Delete Modal */} @@ -223,89 +471,159 @@ function orgPostCard(props: InterfaceOrgPostCardProps): JSX.Element { {/* Edit Modal */} - - -
{t('editPost')}
- + + + {t('editPost')} -
+ -
- - -
-
- - -
-
- - -
-
- - -
+ {t('postTitle')} + + {t('information')} + + {props.postPhoto && ( + <> + {t('image')} + + ): Promise => { + setPostFormState((prevPostFormState) => ({ + ...prevPostFormState, + postphoto: '', + })); + + const file = e.target.files?.[0]; + if (file) { + setPostFormState({ + ...postformState, + postphoto: await convertToBase64(file), + }); + } + }} + /> + {props.postPhoto && ( + <> + {postformState.postphoto && ( +
+ Post Image Preview + +
+ )} + + )} + + )} + {props.postVideo && ( + <> + {t('video')} + + ): Promise => { + setPostFormState((prevPostFormState) => ({ + ...prevPostFormState, + postvideo: '', + })); + const target = e.target as HTMLInputElement; + const file = target.files && target.files[0]; + if (file) { + const videoBase64 = await convertToBase64(file); + setPostFormState({ + ...postformState, + postvideo: videoBase64, + }); + } + }} + /> + {postformState.postvideo && ( +
+ + +
+ )} + + )}
- + -
+
); } -export default orgPostCard; diff --git a/src/screens/OrgPost/OrgPost.module.css b/src/screens/OrgPost/OrgPost.module.css index 037e5f40c8..f9ff4a0794 100644 --- a/src/screens/OrgPost/OrgPost.module.css +++ b/src/screens/OrgPost/OrgPost.module.css @@ -1,85 +1,51 @@ -.navbarbg { - height: 60px; - background-color: white; +.mainpage { display: flex; - margin-bottom: 30px; - z-index: 1; - position: relative; flex-direction: row; - justify-content: space-between; - box-shadow: 0px 0px 8px 2px #c8c8c8; } - -.checkboxdiv { - position: relative; +.btnsContainer { display: flex; + margin: 2.5rem 0 2.5rem 0; } -.checkboxdiv label { - margin-right: 4px; -} - -.checkboxdiv input { - margin-right: 10px; +.btnsContainer .btnsBlock { + display: flex; } -.logo { - color: #707070; - margin-left: 0; +.btnsContainer .btnsBlock button { + margin-left: 1rem; display: flex; + justify-content: center; align-items: center; - text-decoration: none; } -.logo img { - margin-top: 0px; - margin-left: 10px; - height: 64px; - width: 70px; +.btnsContainer .input { + flex: 1; + position: relative; } -.logo > strong { - line-height: 1.5rem; - margin-left: -5px; - font-family: sans-serif; - font-size: 19px; - color: #707070; -} -.mainpage { - display: flex; - flex-direction: row; +.btnsContainer input { + outline: 1px solid var(--bs-gray-400); } -.sidebar { - z-index: 0; - padding-top: 5px; - margin: 0; - height: 100%; -} -.sidebar:after { - content: ''; - background-color: #f7f7f7; - position: absolute; - width: 2px; - height: 600px; - top: 10px; - left: 94%; - display: block; + +.btnsContainer .input button { + width: 52px; } -.sidebarsticky { - padding-left: 45px; - margin-top: 7px; + +.preview { + display: flex; + position: relative; + width: 100%; + margin-top: 10px; + justify-content: center; } -.sidebarsticky > p { - margin-top: -10px; +.preview img { + width: 400px; + height: auto; } - -.navitem { - padding-left: 27%; - padding-top: 12px; - padding-bottom: 12px; - cursor: pointer; +.preview video { + width: 400px; + height: auto; } - .logintitle { color: #707070; font-weight: 600; @@ -89,16 +55,6 @@ border-bottom: 3px solid #31bb6b; width: 15%; } -.searchtitle { - color: #707070; - font-weight: 600; - font-size: 18px; - margin-bottom: 20px; - line-height: 25px; - padding-bottom: 5px; - border-bottom: 3px solid #31bb6b; - width: 60%; -} .logintitleadmin { color: #707070; font-weight: 600; @@ -128,10 +84,69 @@ .justifysp { display: flex; justify-content: space-between; + align-items: center; + margin-bottom: 3rem; +} + +@media (max-width: 1120px) { + .contract { + padding-left: calc(250px + 2rem + 1.5rem); + } + + .listBox .itemCard { + width: 100%; + } +} + +@media (max-width: 1020px) { + .btnsContainer { + flex-direction: column; + margin: 1.5rem 0; + } + + .btnsContainer .btnsBlock { + margin: 1.5rem 0 0 0; + justify-content: space-between; + } + + .btnsContainer .btnsBlock button { + margin: 0; + } + + .btnsContainer .btnsBlock div button { + margin-right: 1.5rem; + } +} + +/* For mobile devices */ + +@media (max-width: 520px) { + .btnsContainer { + margin-bottom: 0; + } + + .btnsContainer .btnsBlock { + display: block; + margin-top: 1rem; + margin-right: 0; + } + + .btnsContainer .btnsBlock div { + flex: 1; + } + + .btnsContainer .btnsBlock div[title='Sort organizations'] { + margin-right: 0.5rem; + } + + .btnsContainer .btnsBlock button { + margin-bottom: 1rem; + margin-right: 0; + width: 100%; + } } @media screen and (max-width: 575.5px) { .justifysp { - padding-left: 55px; display: flex; justify-content: space-between; width: 100%; @@ -147,6 +162,7 @@ border-radius: 5px; font-size: 16px; height: 60%; + width: 60%; color: white; outline: none; font-weight: 600; @@ -159,7 +175,6 @@ justify-content: space-between; border: none; } - .form_wrapper { margin-top: 27px; top: 50%; @@ -216,18 +231,19 @@ .modalbody { width: 50px; } -.greenregbtn { - border: 1px solid #e8e5e5; - box-shadow: 0 2px 2px #e8e5e5; - border-radius: 5px; - background-color: #31bb6b; - font-size: 16px; - height: 60%; - color: white; - outline: none; + +.closeButton { + position: absolute; + top: 0px; + right: 0px; + background: transparent; + transform: scale(1.2); + cursor: pointer; + border: none; + color: #707070; font-weight: 600; + font-size: 16px; cursor: pointer; - transition: transform 0.2s, box-shadow 0.2s; } .sidebarsticky > input { text-decoration: none; @@ -249,7 +265,9 @@ box-shadow: 0 0 5pt 0.5pt #d3d3d3; outline: none; } - +button[data-testid='createPostBtn'] { + display: block; +} .loader, .loader:after { border-radius: 50%; diff --git a/src/screens/OrgPost/OrgPost.test.tsx b/src/screens/OrgPost/OrgPost.test.tsx index 1db08a4179..93f603d446 100644 --- a/src/screens/OrgPost/OrgPost.test.tsx +++ b/src/screens/OrgPost/OrgPost.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { MockedProvider } from '@apollo/react-testing'; import { BrowserRouter } from 'react-router-dom'; -import { act, render, screen } from '@testing-library/react'; +import { act, render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import 'jest-location-mock'; @@ -14,6 +14,7 @@ import { CREATE_POST_MUTATION } from 'GraphQl/Mutations/mutations'; import i18nForTest from 'utils/i18nForTest'; import { StaticMockLink } from 'utils/StaticMockLink'; import { ToastContainer } from 'react-toastify'; +import { debug } from 'jest-preview'; const MOCKS = [ { @@ -35,6 +36,7 @@ const MOCKS = [ text: 'THis is the frist post', imageUrl: null, videoUrl: null, + createdAt: '2023-08-24T09:26:56.524+00:00', creator: { _id: '640d98d9eb6a743d75341067', firstName: 'Aditya', @@ -44,6 +46,7 @@ const MOCKS = [ likeCount: 0, commentCount: 0, comments: [], + pinned: false, likedBy: [], }, { @@ -52,6 +55,7 @@ const MOCKS = [ text: 'THis is the post two', imageUrl: null, videoUrl: null, + createdAt: '2023-08-24T09:26:56.524+00:00', creator: { _id: '640d98d9eb6a743d75341067', firstName: 'Aditya', @@ -60,6 +64,7 @@ const MOCKS = [ }, likeCount: 0, commentCount: 0, + pinned: false, likedBy: [], comments: [], }, @@ -104,7 +109,6 @@ const MOCKS = [ }, ]; const link = new StaticMockLink(MOCKS, true); -const link2 = new StaticMockLink([], true); async function wait(ms = 500): Promise { await act(() => { @@ -119,12 +123,7 @@ describe('Organisation Post Page', () => { posttitle: 'dummy post', postinfo: 'This is a dummy post', postImage: new File(['hello'], 'hello.png', { type: 'image/png' }), - }; - - const formDataEmpty = { - posttitle: ' ', - postinfo: ' ', - postImage: new File(['hello'], 'hello.png', { type: 'image/png' }), + postVideo: new File(['hello'], 'hello.mp4', { type: 'video/mp4' }), }; test('correct mock data should be queried', async () => { @@ -137,6 +136,7 @@ describe('Organisation Post Page', () => { text: 'THis is the frist post', imageUrl: null, videoUrl: null, + createdAt: '2023-08-24T09:26:56.524+00:00', creator: { _id: '640d98d9eb6a743d75341067', firstName: 'Aditya', @@ -145,13 +145,14 @@ describe('Organisation Post Page', () => { }, likeCount: 0, commentCount: 0, - comments: [], + pinned: false, likedBy: [], + comments: [], }); }); - test('should render props and text elements test for the screen', async () => { - const { container } = render( + test('Testing create post functionality', async () => { + render( @@ -163,17 +164,31 @@ describe('Organisation Post Page', () => { ); - expect(container.textContent).not.toBe('Loading data...'); + await wait(); + + userEvent.click(screen.getByTestId('createPostModalBtn')); + + userEvent.type(screen.getByTestId('modalTitle'), formData.posttitle); + + userEvent.type(screen.getByTestId('modalinfo'), formData.postinfo); + userEvent.upload( + screen.getByTestId('organisationImage'), + formData.postImage + ); + userEvent.upload( + screen.getByTestId('organisationImage'), + formData.postVideo + ); + + userEvent.click(screen.getByTestId('createPostBtn')); await wait(); - expect(container.textContent).toMatch('Search Post'); - expect(container.textContent).toMatch('Posts'); - expect(container.textContent).toMatch('+ Create Post'); - }); - // Test : Render two radio buttons - test('should render two radio buttons', async () => { - const { container } = render( + userEvent.click(screen.getByTestId('closeOrganizationModal')); + }, 15000); + + test('Testing search functionality', async () => { + render( @@ -184,21 +199,123 @@ describe('Organisation Post Page', () => { ); + async function debounceWait(ms = 200): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); + } + await debounceWait(); + userEvent.type(screen.getByPlaceholderText(/Search By/i), 'postone'); + await debounceWait(); + const sortDropdown = screen.getByTestId('sort'); + userEvent.click(sortDropdown); + }); + test('Testing search text and title toggle', async () => { + await act(async () => { + // Wrap the test code in act + render( + + + + + + + + + + ); + + await wait(); + + const searchInput = screen.getByTestId('searchByName'); + expect(searchInput).toHaveAttribute('placeholder', 'Search By Title'); + + const inputText = screen.getByTestId('searchBy'); + + fireEvent.click(inputText); + const toggleText = screen.getByTestId('Text'); + + fireEvent.click(toggleText); + + expect(searchInput).toHaveAttribute('placeholder', 'Search By Text'); + fireEvent.click(inputText); + const toggleTite = screen.getByTestId('searchTitle'); + fireEvent.click(toggleTite); + expect(searchInput).toHaveAttribute('placeholder', 'Search By Title'); + }); + }); + test('Testing search latest and oldest toggle', async () => { + await act(async () => { + // Wrap the test code in act + render( + + + + + + + + + + ); + + await wait(); + + const searchInput = screen.getByTestId('sort'); + expect(searchInput).toBeInTheDocument(); + + const inputText = screen.getByTestId('sortpost'); + + fireEvent.click(inputText); + const toggleText = screen.getByTestId('latest'); + + fireEvent.click(toggleText); + + expect(searchInput).toBeInTheDocument(); + fireEvent.click(inputText); + const toggleTite = screen.getByTestId('oldest'); + fireEvent.click(toggleTite); + expect(searchInput).toBeInTheDocument(); + }); + }); + test('After creating a post, the data should be refetched', async () => { + const refetchMock = jest.fn(); - expect(container.textContent).not.toBe('Loading data...'); + render( + + + + + + + + + + + ); await wait(); - expect(container.textContent).toMatch('Title'); - expect(container.textContent).toMatch('Text'); + userEvent.click(screen.getByTestId('createPostModalBtn')); + + // Fill in post form fields... + + userEvent.click(screen.getByTestId('createPostBtn')); + + await wait(); + + expect(refetchMock).toHaveBeenCalledTimes(0); }); - test('Testing create post functionality', async () => { + test('Create post without media', async () => { render( + @@ -207,29 +324,22 @@ describe('Organisation Post Page', () => { ); await wait(); - userEvent.click(screen.getByTestId('createPostModalBtn')); - userEvent.type( - screen.getByPlaceholderText(/Post Title/i), - formData.posttitle - ); - - userEvent.type( - screen.getByPlaceholderText(/What do you to talk about?/i), - formData.postinfo - ); - userEvent.upload(screen.getByLabelText(/Post Image:/i), formData.postImage); + const postTitleInput = screen.getByTestId('modalTitle'); + fireEvent.change(postTitleInput, { target: { value: 'Test Post' } }); - userEvent.click(screen.getByTestId('createPostBtn')); - - await wait(); + const postInfoTextarea = screen.getByTestId('modalinfo'); + fireEvent.change(postInfoTextarea, { + target: { value: 'Test post information' }, + }); - userEvent.click(screen.getByTestId('closePostModalBtn')); + const createPostBtn = screen.getByTestId('createPostBtn'); + fireEvent.click(createPostBtn); }, 15000); - test('Create Post should throw error when empty strings have been entered', async () => { - const { container } = render( + test('Create post and preview', async () => { + render( @@ -243,65 +353,121 @@ describe('Organisation Post Page', () => { ); await wait(); - userEvent.click(screen.getByTestId('createPostModalBtn')); - userEvent.type( - screen.getByPlaceholderText(/Post Title/i), - formDataEmpty.posttitle - ); + const postTitleInput = screen.getByTestId('modalTitle'); + fireEvent.change(postTitleInput, { target: { value: 'Test Post' } }); - userEvent.type( - screen.getByPlaceholderText(/What do you to talk about?/i), - formDataEmpty.postinfo - ); + const postInfoTextarea = screen.getByTestId('modalinfo'); + fireEvent.change(postInfoTextarea, { + target: { value: 'Test post information' }, + }); + const file = new File(['image content'], 'image.png', { + type: 'image/png', + }); + const input = screen.getByTestId('organisationImage'); + userEvent.upload(input, file); - userEvent.upload( - screen.getByLabelText(/Post Image:/i), - formDataEmpty.postImage + await screen.findByAltText('Post Image Preview'); + expect(screen.getByAltText('Post Image Preview')).toBeInTheDocument(); + + const createPostBtn = screen.getByTestId('createPostBtn'); + fireEvent.click(createPostBtn); + debug(); + }, 15000); + + test('Modal opens and closes', async () => { + render( + + + + + + + + + ); - userEvent.click(screen.getByTestId('createPostBtn')); + await wait(); + + const createPostModalBtn = screen.getByTestId('createPostModalBtn'); + + userEvent.click(createPostModalBtn); + + const modalTitle = screen.getByTestId('modalOrganizationHeader'); + expect(modalTitle).toBeInTheDocument(); + + const closeButton = screen.getByTestId('closeOrganizationModal'); + userEvent.click(closeButton); await wait(); - expect(container.textContent).toMatch( - 'Text fields cannot be empty strings' + const closedModalTitle = screen.queryByText(/postDetail/i); + expect(closedModalTitle).not.toBeInTheDocument(); + }); + it('renders the form with input fields and buttons', async () => { + render( + + + + + + + + + + ); - }, 15000); - test('Testing search functionality', async () => { + await wait(); + userEvent.click(screen.getByTestId('createPostModalBtn')); + + // Check if input fields and buttons are present + expect(screen.getByTestId('modalTitle')).toBeInTheDocument(); + expect(screen.getByTestId('modalinfo')).toBeInTheDocument(); + expect(screen.getByTestId('organisationImage')).toBeInTheDocument(); + expect(screen.getByTestId('organisationVideo')).toBeInTheDocument(); + expect(screen.getByTestId('createPostBtn')).toBeInTheDocument(); + }); + + it('allows users to input data into the form fields', async () => { render( + ); - async function debounceWait(ms = 200): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); - } - await debounceWait(); - userEvent.type(screen.getByPlaceholderText(/Search By/i), 'postone'); - await debounceWait(); - }); - test('Testing when post data is not present', async () => { - window.location.assign('/orglist'); + await wait(); + userEvent.click(screen.getByTestId('createPostModalBtn')); + // Simulate user input + fireEvent.change(screen.getByTestId('modalTitle'), { + target: { value: 'Test Title' }, + }); + fireEvent.change(screen.getByTestId('modalinfo'), { + target: { value: 'Test Info' }, + }); + + // Check if input values are set correctly + expect(screen.getByTestId('modalTitle')).toHaveValue('Test Title'); + expect(screen.getByTestId('modalinfo')).toHaveValue('Test Info'); + }); + + test('allows users to upload an image', async () => { render( - + + @@ -310,6 +476,73 @@ describe('Organisation Post Page', () => { ); await wait(); - expect(window.location).toBeAt('/orglist'); + userEvent.click(screen.getByTestId('createPostModalBtn')); + + const postTitleInput = screen.getByTestId('modalTitle'); + fireEvent.change(postTitleInput, { target: { value: 'Test Post' } }); + + const postInfoTextarea = screen.getByTestId('modalinfo'); + fireEvent.change(postInfoTextarea, { + target: { value: 'Test post information' }, + }); + const file = new File(['image content'], 'image.png', { + type: 'image/png', + }); + const input = screen.getByTestId('organisationImage'); + userEvent.upload(input, file); + + await screen.findByAltText('Post Image Preview'); + expect(screen.getByAltText('Post Image Preview')).toBeInTheDocument(); + + const closeButton = screen.getByTestId('closePreview'); + fireEvent.click(closeButton); + }, 15000); + test('Create post, preview image, and close preview', async () => { + await act(async () => { + render( + + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('createPostModalBtn')); + + const postTitleInput = screen.getByTestId('modalTitle'); + fireEvent.change(postTitleInput, { target: { value: 'Test Post' } }); + + const postInfoTextarea = screen.getByTestId('modalinfo'); + fireEvent.change(postInfoTextarea, { + target: { value: 'Test post information' }, + }); + + const videoFile = new File(['video content'], 'video.mp4', { + type: 'video/mp4', + }); + + const videoInput = screen.getByTestId('organisationVideo'); + fireEvent.change(videoInput, { + target: { + files: [videoFile], + }, + }); + + // Check if the video is displayed + const videoPreview = await screen.findByTestId('videoPreview'); + expect(videoPreview).toBeInTheDocument(); + + // Check if the close button for the video works + const closeVideoPreviewButton = screen.getByTestId('videoclosebutton'); + fireEvent.click(closeVideoPreviewButton); + expect(videoPreview).not.toBeInTheDocument(); + }); }); }); diff --git a/src/screens/OrgPost/OrgPost.tsx b/src/screens/OrgPost/OrgPost.tsx index 2e897aede8..9f098c291b 100644 --- a/src/screens/OrgPost/OrgPost.tsx +++ b/src/screens/OrgPost/OrgPost.tsx @@ -1,27 +1,38 @@ import type { ChangeEvent } from 'react'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { Search } from '@mui/icons-material'; +import SortIcon from '@mui/icons-material/Sort'; import Row from 'react-bootstrap/Row'; -import Col from 'react-bootstrap/Col'; import Modal from 'react-bootstrap/Modal'; import { Form } from 'react-bootstrap'; import { useMutation, useQuery } from '@apollo/client'; import Button from 'react-bootstrap/Button'; import { toast } from 'react-toastify'; import { useTranslation } from 'react-i18next'; - +import Dropdown from 'react-bootstrap/Dropdown'; import styles from './OrgPost.module.css'; import OrgPostCard from 'components/OrgPostCard/OrgPostCard'; import { ORGANIZATION_POST_CONNECTION_LIST } from 'GraphQl/Queries/Queries'; import { CREATE_POST_MUTATION } from 'GraphQl/Mutations/mutations'; -import PaginationList from 'components/PaginationList/PaginationList'; import debounce from 'utils/debounce'; import convertToBase64 from 'utils/convertToBase64'; import NotFound from 'components/NotFound/NotFound'; -import { Form as StyleBox } from 'react-bootstrap'; import { errorHandler } from 'utils/errorHandler'; import Loader from 'components/Loader/Loader'; import OrganizationScreen from 'components/OrganizationScreen/OrganizationScreen'; +interface InterfaceOrgPost { + _id: string; + title: string; + text: string; + imageUrl: string; + videoUrl: string; + organizationId: string; + creator: { firstName: string; lastName: string }; + pinned: boolean; + createdAt: string; +} + function orgPost(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'orgPost', @@ -33,15 +44,11 @@ function orgPost(): JSX.Element { posttitle: '', postinfo: '', postImage: '', + postVideo: '', }); + const [sortingOption, setSortingOption] = useState('latest'); const [showTitle, setShowTitle] = useState(true); - const searchChange = (ev: any): void => { - setShowTitle(ev.target.value === 'searchTitle'); - }; - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(5); - const currentUrl = window.location.href.split('=')[1]; const showInviteModal = (): void => { @@ -49,6 +56,12 @@ function orgPost(): JSX.Element { }; const hideInviteModal = (): void => { setPostModalIsOpen(false); + setPostFormState({ + posttitle: '', + postinfo: '', + postImage: '', + postVideo: '', + }); }; const { @@ -61,7 +74,19 @@ function orgPost(): JSX.Element { }); const [create, { loading: createPostLoading }] = useMutation(CREATE_POST_MUTATION); + const [displayedPosts, setDisplayedPosts] = useState( + orgPostListData?.postsByOrganizationConnection.edges || [] + ); + useEffect(() => { + if (orgPostListData && orgPostListData.postsByOrganizationConnection) { + const newDisplayedPosts = sortPosts( + orgPostListData.postsByOrganizationConnection.edges, + sortingOption + ); + setDisplayedPosts(newDisplayedPosts); + } + }, [orgPostListData, sortingOption]); const createPost = async (e: ChangeEvent): Promise => { e.preventDefault(); @@ -69,6 +94,7 @@ function orgPost(): JSX.Element { posttitle: _posttitle, postinfo: _postinfo, postImage, + postVideo, } = postformState; const posttitle = _posttitle.trim(); @@ -84,7 +110,7 @@ function orgPost(): JSX.Element { title: posttitle, text: postinfo, organizationId: currentUrl, - file: postImage, + file: postImage || postVideo, }, }); /* istanbul ignore next */ @@ -95,6 +121,7 @@ function orgPost(): JSX.Element { posttitle: '', postinfo: '', postImage: '', + postVideo: '', }); setPostModalIsOpen(false); // close the modal } @@ -113,21 +140,6 @@ function orgPost(): JSX.Element { window.location.assign('/orglist'); } - /* istanbul ignore next */ - const handleChangePage = ( - event: React.MouseEvent | null, - newPage: number - ): void => { - setPage(newPage); - }; - /* istanbul ignore next */ - const handleChangeRowsPerPage = ( - event: React.ChangeEvent - ): void => { - setRowsPerPage(parseInt(event.target.value, 10)); - setPage(0); - }; - const handleSearch = (e: any): void => { const { value } = e.target; const filterData = { @@ -140,143 +152,197 @@ function orgPost(): JSX.Element { const debouncedHandleSearch = debounce(handleSearch); + const handleSorting = (option: string): void => { + setSortingOption(option); + }; + + const sortPosts = ( + posts: InterfaceOrgPost[], + sortingOption: string + ): InterfaceOrgPost[] => { + const sortedPosts = [...posts]; + + if (sortingOption === 'latest') { + sortedPosts.sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); + } else if (sortingOption === 'oldest') { + sortedPosts.sort( + (a, b) => + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); + } + + return sortedPosts; + }; + + const sortedPostsList: InterfaceOrgPost[] = [...displayedPosts]; + sortedPostsList.sort((a: InterfaceOrgPost, b: InterfaceOrgPost) => { + if (a.pinned === b.pinned) { + return 0; + } + + if (a.pinned) { + return -1; + } + return 1; + }); return ( <> - - -
-
-
{t('searchPost')}
-
-
- - -
-
+ +
+
+
+
-
- - -
- -

{t('posts')}

+
+
+ + +
+ - -
- {orgPostListData && - orgPostListData.postsByOrganizationConnection.edges.length > - 0 ? ( - (rowsPerPage > 0 - ? orgPostListData.postsByOrganizationConnection.edges.slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage - ) - : rowsPerPage > 0 - ? orgPostListData.postsByOrganizationConnection.edges.slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage - ) - : orgPostListData.postsByOrganizationConnection.edges - ).map( - (datas: { - _id: string; - title: string; - text: string; - imageUrl: string; - videoUrl: string; - organizationId: string; - creator: { firstName: string; lastName: string }; - }) => { - return ( - - ); - } - ) - ) : ( - - )}
-
- - - - + {sortedPostsList && sortedPostsList.length > 0 ? ( + sortedPostsList.map( + (datas: { + _id: string; + title: string; + text: string; + imageUrl: string; + videoUrl: string; + organizationId: string; + creator: { firstName: string; lastName: string }; + pinned: boolean; + }) => ( + - - -
+ ) + ) + ) : ( + + )}
- +
- - -

{t('postDetails')}

- + + + {t('postDetails')} - -
- + + + {t('postTitle')} - -