From a6ced9ab2c144ab7642a423f6660460cd420965f Mon Sep 17 00:00:00 2001 From: Siddhesh Bhupendra Kuakde Date: Wed, 6 Sep 2023 22:52:40 +0530 Subject: [PATCH 1/4] Feature Request: Adding a dialog to go to plugin store after an organization is created by the admin (#951) * Add/ test for OrgPost.tsx * fix: org post back to default * Added Dialog 2 * Updated Dialog UI * Removed Extra code * Updated Plugin store * fix: warnings and solves #951 & #948 * fix: warnings and solves #951 & #948 * fix: warnings and solves #951 & #948 * Fix: UI Redesign * fix: merge * fix * Update AddOnStore.tsx * Fixed Merge Errors * Add test: for OrgEntry * Test 3 * fix test 4 --- public/locales/en.json | 9 +- src/GraphQl/Mutations/mutations.ts | 4 +- src/GraphQl/Queries/Queries.ts | 1 + .../AddOn/core/AddOnEntry/AddOnEntry.test.tsx | 39 ++++- .../AddOn/core/AddOnEntry/AddOnEntry.tsx | 134 ++++-------------- .../AddOn/core/AddOnStore/AddOnStore.tsx | 64 ++------- src/screens/OrgList/OrgList.module.css | 79 +++++++++++ src/screens/OrgList/OrgList.tsx | 65 +++++++++ 8 files changed, 227 insertions(+), 168 deletions(-) diff --git a/public/locales/en.json b/public/locales/en.json index bf232e2bdd..80a75b08db 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -86,6 +86,11 @@ "cancel": "Cancel", "noOrgErrorTitle": "Organizations Not Found", "noOrgErrorDescription": "Please create an organization through dashboard", + + "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", + "enableEverything": "Enable Everything", "noResultsFoundFor": "No results found for " }, "orgListCard": { @@ -442,7 +447,9 @@ "addOnEntry": { "enable": "Enabled", "install": "Install", - "uninstall": "Uninstall" + "uninstall": "Uninstall", + "uninstallMsg": "This feature is now removed from your organization", + "installMsg": "This feature is now enabled in your organization" }, "memberDetail": { "title": "User Details", diff --git a/src/GraphQl/Mutations/mutations.ts b/src/GraphQl/Mutations/mutations.ts index 040dea6344..550ad1e07d 100644 --- a/src/GraphQl/Mutations/mutations.ts +++ b/src/GraphQl/Mutations/mutations.ts @@ -351,8 +351,8 @@ export const REJECT_ADMIN_MUTATION = gql` * @description used to toggle `installStatus` (boolean value) of a Plugin */ export const UPDATE_INSTALL_STATUS_PLUGIN_MUTATION = gql` - mutation update_install_status_plugin_mutation($id: ID!, $orgId: ID!) { - updatePluginStatus(orgId: $orgId, id: $id) { + mutation ($id: ID!, $orgId: ID!) { + updatePluginStatus(id: $id, orgId: $orgId) { _id pluginName pluginCreatedBy diff --git a/src/GraphQl/Queries/Queries.ts b/src/GraphQl/Queries/Queries.ts index 49ac11df57..1f47769e4c 100644 --- a/src/GraphQl/Queries/Queries.ts +++ b/src/GraphQl/Queries/Queries.ts @@ -600,6 +600,7 @@ export const PLUGIN_GET = gql` pluginName pluginCreatedBy pluginDesc + uninstalledOrgs } } `; diff --git a/src/components/AddOn/core/AddOnEntry/AddOnEntry.test.tsx b/src/components/AddOn/core/AddOnEntry/AddOnEntry.test.tsx index 1716323755..50e56adf1e 100644 --- a/src/components/AddOn/core/AddOnEntry/AddOnEntry.test.tsx +++ b/src/components/AddOn/core/AddOnEntry/AddOnEntry.test.tsx @@ -23,7 +23,7 @@ const httpLink = new HttpLink({ authorization: 'Bearer ' + localStorage.getItem('token') || '', }, }); - +console.error = jest.fn(); const client: ApolloClient = new ApolloClient({ cache: new InMemoryCache(), link: ApolloLink.from([httpLink]), @@ -52,7 +52,7 @@ describe('Testing AddOnEntry', () => { - {} + {} @@ -60,4 +60,39 @@ describe('Testing AddOnEntry', () => { ); expect(getByTestId('AddOnEntry')).toBeInTheDocument(); }); + + it('renders correctly', () => { + const props = { + id: '1', + title: 'Test Addon', + description: 'Test addon description', + createdBy: 'Test User', + component: 'string', + installed: true, + configurable: true, + modified: true, + isInstalled: true, + uninstalledOrgs: [], + enabled: true, + getInstalledPlugins: (): { sample: string } => { + return { sample: 'sample' }; + }, + }; + + const { getByText } = render( + + + + + {} + + + + + ); + + expect(getByText('Test Addon')).toBeInTheDocument(); + expect(getByText('Test addon description')).toBeInTheDocument(); + expect(getByText('Test User')).toBeInTheDocument(); + }); }); diff --git a/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx b/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx index 9288c30af9..7503d0667d 100644 --- a/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx +++ b/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx @@ -1,13 +1,11 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import styles from './AddOnEntry.module.css'; -import { Button, Card, Form, Spinner } from 'react-bootstrap'; -import { - UPDATE_INSTALL_STATUS_PLUGIN_MUTATION, - UPDATE_ORG_STATUS_PLUGIN_MUTATION, -} from 'GraphQl/Mutations/mutations'; +import { Button, Card, Spinner } from 'react-bootstrap'; +import { UPDATE_INSTALL_STATUS_PLUGIN_MUTATION } from 'GraphQl/Mutations/mutations'; import { useMutation } from '@apollo/client'; import { useTranslation } from 'react-i18next'; +import { toast } from 'react-toastify'; interface InterfaceAddOnEntryProps { id: string; @@ -16,131 +14,52 @@ interface InterfaceAddOnEntryProps { description: string; createdBy: string; component: string; - installed?: boolean; - configurable?: boolean; modified: any; - isInstalled: boolean; + uninstalledOrgs: string[]; getInstalledPlugins: () => any; } function addOnEntry({ id, - enabled, title, description, createdBy, - installed, - isInstalled, + uninstalledOrgs, getInstalledPlugins, }: InterfaceAddOnEntryProps): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'addOnEntry' }); - + //getting orgId from URL + const currentOrg = window.location.href.split('/id=')[1] + ''; const [buttonLoading, setButtonLoading] = useState(false); - const [switchInProgress] = useState(false); - const [isInstalledLocal, setIsInstalledLocal] = useState(isInstalled); - - const [updateInstallStatus] = useMutation( + const [isInstalledLocal, setIsInstalledLocal] = useState( + uninstalledOrgs.includes(currentOrg) + ); + // const [addOrgAsUninstalled] = useMutation(UPDATE_ORG_STATUS_PLUGIN_MUTATION); + const [addOrgAsUninstalled] = useMutation( UPDATE_INSTALL_STATUS_PLUGIN_MUTATION ); - const [updateOrgStatus] = useMutation(UPDATE_ORG_STATUS_PLUGIN_MUTATION); - - const currentOrg = window.location.href.split('=')[1]; - const updateOrgList = async (): Promise => { - await updateOrgStatus({ - variables: { - id: id.toString(), - orgId: currentOrg.toString(), - }, - }); - }; - - const updateInstallStatusFunc = async (): Promise => { + const togglePluginInstall = async (): Promise => { setButtonLoading(true); - await updateInstallStatus({ + await addOrgAsUninstalled({ variables: { id: id.toString(), - status: !isInstalledLocal, + orgId: currentOrg.toString(), }, }); setIsInstalledLocal(!isInstalledLocal); setButtonLoading(false); + const dialog: string = isInstalledLocal + ? t('installMsg') + : t('uninstallMsg'); + toast.success(dialog); }; - // useEffect(() => { - // // updateInstallStatusFunc(); - // }, []); - // TODO: Install/Remove Effect - // 1. Update Server to add to Org - // 2. Validate Permissions - // 3. Trigger Server Hook if Validated. (Stream to track progress) - // const install = () => { - // setButtonLoading(true); - // fetch('http://localhost:3005/installed', { - // method: 'POST', - // headers: { - // 'Content-type': 'application/json; charset=UTF-8', - // }, - // body: JSON.stringify( - // Object.assign( - // {}, - // { ...entry }, - // { - // installedDatetime: new Date(), - // installedBy: 'Admin', - // enabled: true, - // } - // ) - // ), - // }) - // .then(() => { - // setButtonLoading(false); - // modified(); - // }) - // .finally(() => setButtonLoading(false)); - // }; - - // const remove = () => { - // setButtonLoading(true); - // fetch(`http://localhost:3005/installed/${id}`, { - // method: 'DELETE', - // }) - // .then(() => { - // setButtonLoading(false); - // modified(); - // }) - // .finally(() => setButtonLoading(false)); - // }; - - // const toggleActive = () => { - // setSwitchState(true); - // fetch(`http://localhost:3005/installed/${id}`, { - // method: 'PUT', - // headers: { - // 'Content-type': 'application/json; charset=UTF-8', - // }, - // body: JSON.stringify( - // Object.assign( - // {}, - // { ...entry }, - // { - // enabled: !enabled, - // } - // ) - // ), - // }) - // .then(() => { - // modified(); - // setSwitchState(false); - // }) - // .finally(() => setSwitchState(false)); - // }; - return ( <> - {installed && ( + {/* {uninstalledOrgs.includes(currentOrg) && ( - )} + )} */} {title} @@ -163,22 +82,23 @@ function addOnEntry({ variant="primary" // disabled={buttonLoading || !configurable} disabled={buttonLoading} + data-testid="AddOnEntry_btn_install" onClick={(): void => { - updateOrgList(); - updateInstallStatusFunc(); + togglePluginInstall(); getInstalledPlugins(); - // installed ? remove() : install(); }} > {buttonLoading ? ( ) : ( )} {/* {installed ? 'Remove' : configurable ? 'Installed' : 'Install'} */} - {isInstalledLocal ? t('uninstall') : t('install')} + {uninstalledOrgs.includes(currentOrg) + ? t('install') + : t('uninstall')} diff --git a/src/components/AddOn/core/AddOnStore/AddOnStore.tsx b/src/components/AddOn/core/AddOnStore/AddOnStore.tsx index 1a6eaad34d..2e3e511149 100644 --- a/src/components/AddOn/core/AddOnStore/AddOnStore.tsx +++ b/src/components/AddOn/core/AddOnStore/AddOnStore.tsx @@ -240,7 +240,7 @@ function addOnStore(): JSX.Element { pluginName: string | undefined; pluginDesc: string | undefined; pluginCreatedBy: string; - pluginInstallStatus: boolean | undefined; + uninstalledOrgs: string[]; getInstalledPlugins: () => any; }, i: React.Key | null | undefined @@ -251,13 +251,14 @@ function addOnStore(): JSX.Element { title={plug.pluginName} description={plug.pluginDesc} createdBy={plug.pluginCreatedBy} - isInstalled={plug.pluginInstallStatus} - configurable={plug.pluginInstallStatus} + // isInstalled={plug.pluginInstallStatus} + // configurable={plug.pluginInstallStatus} component={'Special Component'} modified={(): void => { console.log('Plugin is modified'); }} getInstalledPlugins={getInstalledPlugins} + uninstalledOrgs={plug.uninstalledOrgs} /> ) ) @@ -319,7 +320,7 @@ function addOnStore(): JSX.Element { pluginName: string | undefined; pluginDesc: string | undefined; pluginCreatedBy: string; - pluginInstallStatus: boolean | undefined; + uninstalledOrgs: string[]; getInstalledPlugins: () => any; }, i: React.Key | null | undefined @@ -330,13 +331,14 @@ function addOnStore(): JSX.Element { title={plug.pluginName} description={plug.pluginDesc} createdBy={plug.pluginCreatedBy} - isInstalled={plug.pluginInstallStatus} - configurable={plug.pluginInstallStatus} + // isInstalled={plug.pluginInstallStatus} + // configurable={plug.pluginInstallStatus} component={'Special Component'} modified={(): void => { console.log('Plugin is modified'); }} getInstalledPlugins={getInstalledPlugins} + uninstalledOrgs={plug.uninstalledOrgs} /> ) ) @@ -356,53 +358,3 @@ addOnStore.defaultProps = {}; addOnStore.propTypes = {}; export default addOnStore; - -// {addonStore.map((plugin: any, index: number) => { -// return ( -// { -// /* istanbul ignore next */ -// pluginModified().then((installedPlugins) => { -// updateLinks( -// new PluginHelper().generateLinks(installedPlugins) -// ); -// }); -// }} -// /> -// ); -// })} - -// {installed -// .filter((plugin: any) => -// showEnabled ? plugin.enabled : !plugin.enabled -// ) -// .map((plugin: any, index: number) => { -// return ( -// { -// /* istanbul ignore next */ -// pluginModified().then((installedPlugins) => { -// updateLinks( -// new PluginHelper().generateLinks(installedPlugins) -// ); -// }); -// }} -// /> -// ); -// })} diff --git a/src/screens/OrgList/OrgList.module.css b/src/screens/OrgList/OrgList.module.css index b7ba2e1314..6e643bb80d 100644 --- a/src/screens/OrgList/OrgList.module.css +++ b/src/screens/OrgList/OrgList.module.css @@ -129,6 +129,34 @@ flex-direction: column; margin-left: 1rem; } +.titlemodaldialog { + color: #707070; + font-size: 20px; + margin-bottom: 20px; + padding-bottom: 5px; +} +form label { + font-weight: bold; + padding-bottom: 1px; + font-size: 14px; + color: #707070; +} + +form > input { + display: block; + margin-bottom: 20px; + border: 1px solid #e8e5e5; + box-shadow: 2px 1px #e8e5e5; + padding: 10px 20px; + border-radius: 5px; + background: none; + width: 100%; + transition: all 0.3s ease-in-out; + -webkit-transition: all 0.3s ease-in-out; + -moz-transition: all 0.3s ease-in-out; + -ms-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; +} .itemCard .loadingWrapper .innerContainer .content h5 { height: 24px; @@ -136,11 +164,62 @@ margin-bottom: 0.8rem; } +.cancel > i { + margin-top: 5px; + transform: scale(1.2); + cursor: pointer; + color: #707070; +} +.modalbody { + width: 50px; +} +.pluginStoreBtnContainer { + display: flex; + gap: 1rem; +} +.greenregbtn { + margin: 1rem 0 0; + margin-top: 10px; + border: 1px solid #e8e5e5; + box-shadow: 0 2px 2px #e8e5e5; + padding: 10px 10px; + border-radius: 5px; + background-color: #31bb6b; + width: 100%; + font-size: 16px; + color: white; + outline: none; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + width: 100%; +} + .itemCard .loadingWrapper .innerContainer .content h6[title='Location'] { display: block; width: 45%; height: 18px; } +.secondbtn { + display: flex; + align-items: center; + justify-content: center; + margin: 1rem 0 0; + margin-top: 10px; + border: 1px solid #d0cfcf; + box-shadow: 0 2px 2px #d0cfcf; + padding: 10px 10px; + border-radius: 5px; + background-color: white; + width: 100%; + font-size: 16px; + color: #31bb6b; + outline: none; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + width: 100%; +} .itemCard .loadingWrapper .innerContainer .content h6 { display: block; diff --git a/src/screens/OrgList/OrgList.tsx b/src/screens/OrgList/OrgList.tsx index 702f8b2d3c..a34a084a14 100644 --- a/src/screens/OrgList/OrgList.tsx +++ b/src/screens/OrgList/OrgList.tsx @@ -25,10 +25,27 @@ import type { } 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' }); + const [dialogModalisOpen, setdialogModalIsOpen] = useState(false); + /* eslint-disable @typescript-eslint/no-unused-vars */ + const [modalisOpen, setmodalIsOpen] = useState(false); + const [dialogRedirectOrgId, setDialogRedirectOrgId] = useState(''); + /* eslint-disable @typescript-eslint/explicit-function-return-type */ + function openDialogModal(redirectOrgId: string) { + setDialogRedirectOrgId(redirectOrgId); + // console.log(redirectOrgId, dialogRedirectOrgId); + setdialogModalIsOpen(true); + } + /* eslint-disable @typescript-eslint/explicit-function-return-type */ + function closeDialogModal() { + setdialogModalIsOpen(false); + } + const toggleDialogModal = (): void => + setdialogModalIsOpen(!dialogModalisOpen); document.title = t('title'); const [searchByName, setSearchByName] = useState(''); @@ -136,6 +153,7 @@ function orgList(): JSX.Element { if (data) { toast.success('Congratulation the Organization is created'); refetchOrgs(); + openDialogModal(data.createOrganization._id); setFormState({ name: '', descrip: '', @@ -437,7 +455,54 @@ function orgList(): JSX.Element { + {' '} + + +
+
+
+

{t('manageFeatures')}

+ + + +
+

+ {t('manageFeaturesInfo')} +

+ +
+ + {t('goToStore')} + + {/* */} + +
+
+
+
+ {/* Plugin Notification after Org is Created */} ); From b1f513fc7108a4686bb59620cfacc6618c2087ae Mon Sep 17 00:00:00 2001 From: Rishav Jha <76212518+rishav-jha-mech@users.noreply.github.com> Date: Sat, 9 Sep 2023 20:16:47 +0530 Subject: [PATCH 2/4] Merge latest AdminUI Redesign into develop (#972) * Initial Organizations screen done * Removed yellow scrollbar * Linting fixed * Replaced images with svgs for logos * Styling done for btnsContainer * Better typechecking and readability * Animated Drawer working * Responsive page ready * OrgCard responsive * Fixed navbar issue and added webkit keyframes * LeftDrawer ready * Translations added * Added shimmer loading effect * Styling issue fixed * Failing tests fixed for OrgList * Removed unused vars * Tests done for LeftDrawer * Succesfully made component without causing any breaking change * 100% Code coverage achieved for Requests Screen * Fix alignment * Roles screen UI done * Role screen fixed with 100% test coverage * Changing screen activeness fixed * Unused vars and Typos fixed * Language support added * Linting and typos fixed * Fixed failing tests for LeftDrawer * Completed tests of AdminDashListCard with 100% code coverage * OrgListCard done * Finalised tests * Requests user search made functional again ! * Fixed loading on refetch and UX on all screens * OrgList failing errors fixed * Fixed all failing tests * Achieved 100% code coverage for OrgList.tsx * Wrote tests and mod LeftDrawer for admins * Minor ui issue fixed * Fixed failing test * UI bug dropdown * Frontend insync with Backend attempt 1 * Introspection fail fix 1 * Introspection error fix 3 * Introspection error fix another attempt * Another attempt * Fixed Default Animation on Organizations Screen * Fixed typo * Loading data from localstorage functional * Fixed name conventions * Fixed typo * UI Fix * Changed screen name * Table Loader added * Added LeftOrg drawer and Organization screen comp to Screens * routesReducer tests fixed * Redundant adminNavbar removed from project * MemberDetail issue fixed * Achieved 100% code coverage for LeftDrawerOrg, Added Empty div in images * Fixed failing tests * Fix tests * Fixed warnings * Linting fixes * Linting issues fixed * Achieved 100% code coverage for CollapsibleDropdown * Achieved 100% CC on IconComponent and removed useless imports * Achieved 100% cc for LeftDrawer Component * Achieved 100% CC on SuperAdminScreen Component * Fixed typo * Integrated Event Dashboard * Failing tests for LeftDrawer LeftDrawerOrg OrgList screen fixed * Removed redundant code * Removed useless imports * Linting fixed * Removed LeftDrawerOrg * Dashboard screen ui almost ready * Org Dash ready * Block/Unblock screen ready * Organization settings page ready * Lang changes * Page refresh on updating org removed * OrgUpdate tests done * OrgUpdate 100% test coverage achieved * OrgSettings Tests done * Organization Dashboard Cards done * Organization Dashboard achieved 100% CC * 100% CC achieved for BlockUser screen * Finalised changes * Small change * Tests fixed * Separate OrgSettings component made * Linting fixed * Formatting fixed --- public/locales/en.json | 35 +- public/locales/fr.json | 31 +- public/locales/hi.json | 30 +- public/locales/sp.json | 30 +- public/locales/zh.json | 30 +- src/GraphQl/Queries/Queries.ts | 2 + src/assets/svgs/admin.svg | 5 + src/assets/svgs/{icons => }/angleRight.svg | 0 src/assets/svgs/{icons => }/blockUser.svg | 0 src/assets/svgs/blockedUser.svg | 3 + src/assets/svgs/{icons => }/dashboard.svg | 0 src/assets/svgs/event.svg | 3 + src/assets/svgs/{icons => }/events.svg | 0 src/assets/svgs/{icons => }/logout.svg | 0 src/assets/svgs/{icons => }/organizations.svg | 0 src/assets/svgs/{icons => }/people.svg | 0 src/assets/svgs/{icons => }/plugins.svg | 0 src/assets/svgs/post.svg | 3 + src/assets/svgs/{icons => }/posts.svg | 0 src/assets/svgs/{icons => }/requests.svg | 0 src/assets/svgs/{icons => }/roles.svg | 0 src/assets/svgs/{icons => }/settings.svg | 0 src/assets/svgs/{icons => }/tags.svg | 0 src/assets/svgs/users.svg | 3 + .../ChangeLanguageDropDown.tsx | 7 +- .../ChangeLanguageDropdown.module.css | 7 - src/components/DeleteOrg/DeleteOrg.module.css | 25 + src/components/DeleteOrg/DeleteOrg.test.tsx | 84 +++ src/components/DeleteOrg/DeleteOrg.tsx | 89 ++++ .../IconComponent/IconComponent.tsx | 16 +- src/components/LeftDrawer/LeftDrawer.tsx | 12 +- .../LeftDrawerOrg/LeftDrawerOrg.test.tsx | 10 +- .../LeftDrawerOrg/LeftDrawerOrg.tsx | 48 +- src/components/Loader/Loader.module.css | 14 +- src/components/Loader/Loader.tsx | 22 +- src/components/OrgUpdate/OrgUpdate.module.css | 113 +--- src/components/OrgUpdate/OrgUpdate.test.tsx | 278 +++++----- src/components/OrgUpdate/OrgUpdate.tsx | 274 +++++----- src/components/OrgUpdate/OrgUpdateMocks.ts | 157 ++++++ .../OrganizationDashCards/CardItem.module.css | 47 ++ .../OrganizationDashCards/CardItem.test.tsx | 42 ++ .../OrganizationDashCards/CardItem.tsx | 48 ++ .../OrganizationDashCards/CardItemLoading.tsx | 24 + .../DashboardCard.test.tsx | 18 + .../OrganizationDashCards/DashboardCard.tsx | 32 ++ .../DashboardCardLoading.tsx | 36 ++ .../Dashboardcard.module.css | 60 +++ src/screens/BlockUser/BlockUser.module.css | 196 ++----- src/screens/BlockUser/BlockUser.test.tsx | 208 +++++--- src/screens/BlockUser/BlockUser.tsx | 370 ++++++------- .../ForgotPassword/ForgotPassword.module.css | 17 - src/screens/ForgotPassword/ForgotPassword.tsx | 3 +- .../OrgSettings/OrgSettings.module.css | 189 +------ src/screens/OrgSettings/OrgSettings.test.tsx | 234 +++----- src/screens/OrgSettings/OrgSettings.tsx | 196 +------ .../OrganizationDashboard.module.css | 201 +------ .../OrganizationDashboard.test.tsx | 241 ++++----- .../OrganizationDashboard.tsx | 502 ++++++++---------- .../OrganizationDashboardMocks.ts | 323 ++++------- src/utils/interfaces.ts | 40 ++ 60 files changed, 2116 insertions(+), 2242 deletions(-) create mode 100644 src/assets/svgs/admin.svg rename src/assets/svgs/{icons => }/angleRight.svg (100%) rename src/assets/svgs/{icons => }/blockUser.svg (100%) create mode 100644 src/assets/svgs/blockedUser.svg rename src/assets/svgs/{icons => }/dashboard.svg (100%) create mode 100644 src/assets/svgs/event.svg rename src/assets/svgs/{icons => }/events.svg (100%) rename src/assets/svgs/{icons => }/logout.svg (100%) rename src/assets/svgs/{icons => }/organizations.svg (100%) rename src/assets/svgs/{icons => }/people.svg (100%) rename src/assets/svgs/{icons => }/plugins.svg (100%) create mode 100644 src/assets/svgs/post.svg rename src/assets/svgs/{icons => }/posts.svg (100%) rename src/assets/svgs/{icons => }/requests.svg (100%) rename src/assets/svgs/{icons => }/roles.svg (100%) rename src/assets/svgs/{icons => }/settings.svg (100%) rename src/assets/svgs/{icons => }/tags.svg (100%) create mode 100644 src/assets/svgs/users.svg delete mode 100644 src/components/ChangeLanguageDropdown/ChangeLanguageDropdown.module.css create mode 100644 src/components/DeleteOrg/DeleteOrg.module.css create mode 100644 src/components/DeleteOrg/DeleteOrg.test.tsx create mode 100644 src/components/DeleteOrg/DeleteOrg.tsx create mode 100644 src/components/OrgUpdate/OrgUpdateMocks.ts create mode 100644 src/components/OrganizationDashCards/CardItem.module.css create mode 100644 src/components/OrganizationDashCards/CardItem.test.tsx create mode 100644 src/components/OrganizationDashCards/CardItem.tsx create mode 100644 src/components/OrganizationDashCards/CardItemLoading.tsx create mode 100644 src/components/OrganizationDashCards/DashboardCard.test.tsx create mode 100644 src/components/OrganizationDashCards/DashboardCard.tsx create mode 100644 src/components/OrganizationDashCards/DashboardCardLoading.tsx create mode 100644 src/components/OrganizationDashCards/Dashboardcard.module.css diff --git a/public/locales/en.json b/public/locales/en.json index 80a75b08db..6dd9156052 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -152,11 +152,7 @@ "posts": "Posts", "events": "Events", "blockedUsers": "Blocked Users", - "membershipRequests": "Membership Requests", - "deleteOrganization": "Delete Organization", - "deleteMsg": "Do you want to delete this organization?", - "no": "No", - "yes": "Yes", + "requests": "Requests", "talawaApiUnavailable": "Talawa-API service is unavailable. Is it running? Check your network connectivity too." }, "organizationPeople": { @@ -304,6 +300,7 @@ }, "blockUnblockUser": { "title": "Talawa Block/Unblock User", + "pageName": "Block/Unblock", "searchByName": "Search By Name", "listOfUsers": "List of Users who spammed", "name": "Name", @@ -317,8 +314,10 @@ "talawaApiUnavailable": "Talawa-API service is unavailable. Is it running? Check your network connectivity too.", "allMembers": "All Members", "blockedUsers": "Blocked Users", - "searchFirstName": "Enter First Name", - "searchLastName": "Enter Last Name" + "searchByFirstName": "Search By First Name", + "searchByLastName": "Search By Last Name", + "noResultsFoundFor": "No results found for", + "noSpammerFound": "No spammer found" }, "forgotPassword": { "title": "Talawa Forgot Password", @@ -367,13 +366,20 @@ }, "orgSettings": { "title": "Talawa Setting", - "updateYourDetails": "Update Your Details", - "updateYourPassword": "Update Your Password", + "pageName": "Settings", "updateOrganization": "Update Organization", - "deleteOrganization": "Delete Organization", "seeRequest": "See Request", "settings": "Settings", - "noData": "No data" + "noData": "No data", + "otherSettings": "Other Settings", + "changeLanguage": "Change Language" + }, + "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." }, "userUpdate": { "firstName": "First Name", @@ -388,7 +394,6 @@ "saveChanges": "Save Changes", "cancel": "Cancel" }, - "userPasswordUpdate": { "previousPassword": "Previous Password", "newPassword": "New Password", @@ -396,7 +401,6 @@ "saveChanges": "Save Changes", "cancel": "Cancel" }, - "orgDelete": { "deleteOrg": "Delete Org" }, @@ -412,10 +416,9 @@ "description": "Description", "location": "Location", "displayImage": "Display Image", - "isPublic": "Is Public", - "isRegistrable": "Is Registrable", + "isPublic": "Public", + "isVisibleInSearch": "Visible in Search", "saveChanges": "Save Changes", - "cancel": "Cancel", "enterNameOrganization": "Enter Organization Name", "successfulUpdated": "Organization updated successfully", "talawaApiUnavailable": "Talawa-API service is unavailable. Is it running? Check your network connectivity too." diff --git a/public/locales/fr.json b/public/locales/fr.json index 6d501840ac..78d3fade9b 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -145,11 +145,7 @@ "posts": "Des postes", "events": "Événements", "blockedUsers": "Utilisateurs bloqués", - "membershipRequests": "Demandes d'adhésion", - "deleteOrganization": "Supprimer l'organisation", - "deleteMsg": "Voulez-vous supprimer cette organisation ?", - "no": "Non", - "yes": "Oui", + "requests": "Demandes", "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Est-il en cours d'exécution ? Vérifiez également votre connectivité réseau." }, "organizationPeople": { @@ -297,6 +293,7 @@ }, "blockUnblockUser": { "title": "Talawa Bloquer/Débloquer l'utilisateur", + "pageName": "Bloquer/Débloquer'", "searchByName": "Recherche par nom", "listOfUsers": "Liste des utilisateurs qui ont spammé", "name": "Nom", @@ -310,8 +307,10 @@ "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Est-il en cours d'exécution ? Vérifiez également votre connectivité réseau.", "allMembers": "Tous les membres", "blockedUsers": "Utilisateurs bloqués", - "searchFirstName": "Entrez votre prénom", - "searchLastName": "Entrer le nom de famille" + "searchByFirstName": "Rechercher par prénom", + "searchByLastName": "Rechercher par nom de famille", + "noResultsFoundFor": "Aucun résultat trouvé pour ", + "noSpammerFound": "Aucun spammeur trouvé" }, "forgotPassword": { "title": "Mot de passe oublié Talawa", @@ -360,13 +359,22 @@ }, "orgSettings": { "title": "Paramètre Talawa", + "pageName": "Paramètres", "updateYourDetails": "Mettre à jour vos informations", "updateYourPassword": "Mettez à jour votre mot de passe", "updateOrganization": "Mettre à jour l'organisation", - "deleteOrganization": "Supprimer l'organisation", "seeRequest": "Voir demande", "settings": "Réglages", - "noData": "Pas de données" + "noData": "Pas de données", + "otherSettings": "Autres paramètres", + "changeLanguage": "Changer la langue" + }, + "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." }, "userUpdate": { "firstName": "Prénom", @@ -403,10 +411,9 @@ "description": "La description", "location": "emplacement", "displayImage": "Afficher l'image", - "isPublic": "Est publique", - "isRegistrable": "Est enregistrable", + "isPublic": "Public", + "isVisibleInSearch": "Visible dans la recherche", "saveChanges": "Sauvegarder les modifications", - "cancel": "Annuler", "enterNameOrganization": "Entrez le nom de l'organisation", "successfulUpdated": "Mise à jour réussie", "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Est-il en cours d'exécution ? Vérifiez également votre connectivité réseau." diff --git a/public/locales/hi.json b/public/locales/hi.json index d376cb1107..29b449a0c9 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -145,11 +145,7 @@ "posts": "पोस्ट", "events": "आयोजन", "blockedUsers": "रोके गए उपयोगकर्ता", - "membershipRequests": "सदस्यता अनुरोध", - "deleteOrganization": "संगठन हटाएं", - "deleteMsg": "क्या आप इस संगठन को हटाना चाहते हैं?", - "no": "नहीं", - "yes": "हाँ", + "requests": "अनुरोध", "talawaApiUnavailable": "तलवा-एपीआई सेवा उपलब्ध नहीं है। क्या यह चल रहा है? अपनी नेटवर्क कनेक्टिविटी भी जांचें।" }, "organizationPeople": { @@ -297,6 +293,7 @@ }, "blockUnblockUser": { "title": "तलावा ब्लॉक/अनब्लॉक यूजर", + "pageName": "ब्लॉक/अनब्लॉक", "searchByName": "नाम से खोजें", "listOfUsers": "स्पैम करने वाले उपयोगकर्ताओं की सूची", "name": "नाम", @@ -310,8 +307,10 @@ "talawaApiUnavailable": "तलवा-एपीआई सेवा उपलब्ध नहीं है। क्या यह चल रहा है? अपनी नेटवर्क कनेक्टिविटी भी जांचें।", "allMembers": "सभी सदस्य", "blockedUsers": "रोके गए उपयोगकर्ता", - "searchFirstName": "प्रथम नाम दर्ज करें", - "searchLastName": "अंतिम नाम दर्ज करो" + "searchByFirstName": "पहले नाम से खोजें", + "searchByLastName": "उपनाम से खोजें", + "noResultsFoundFor": "के लिए कोई परिणाम नहीं मिला ", + "noSpammerFound": "कोई स्पैमर नहीं मिला" }, "forgotPassword": { "title": "तलवा पासवर्ड भूल गए", @@ -360,13 +359,22 @@ }, "orgSettings": { "title": "तलावा सेटिंग", + "pageName": "सेटिंग्स", "updateYourDetails": "अपना विवरण अपडेट करें", "updateYourPassword": "अपना पासवर्ड अपडेट करें", "updateOrganization": "अद्यतन संगठन", - "deleteOrganization": "संगठन हटाएं", "seeRequest": "अनुरोध देखें", "settings": "समायोजन", - "noData": "कोई डेटा नहीं" + "noData": "कोई डेटा नहीं", + "otherSettings": "अन्य सेटिंग्स", + "changeLanguage": "भाषा बदलें" + }, + "deleteOrg": { + "deleteOrganization": "संगठन हटाएं", + "deleteMsg": "क्या आप इस संगठन को हटाना चाहते हैं?", + "no": "नहीं", + "yes": "हां", + "longDelOrgMsg": "संगठन हटाने के बटन पर क्लिक करके, संगठन को स्थायित रूप से हटा दिया जाएगा, साथ ही उसके आयोजन, टैग और सभी संबंधित डेटा भी हटा दिया जाएगा।" }, "userUpdate": { "firstName": "पहला नाम", @@ -403,8 +411,8 @@ "description": "विवरण", "location": "जगह", "displayImage": "प्रदर्शन छवि", - "isPublic": "सार्वजनिक है", - "isRegistrable": "पंजीकरण योग्य है", + "isPublic": "सार्वजनिक", + "isVisibleInSearch": "खोज में दिखाए जा सकते हैं", "saveChanges": "परिवर्तनों को सुरक्षित करें", "cancel": "रद्द करना", "enterNameOrganization": "संगठन का नाम दर्ज करें", diff --git a/public/locales/sp.json b/public/locales/sp.json index af9a7f5686..7fa6ccd04b 100644 --- a/public/locales/sp.json +++ b/public/locales/sp.json @@ -145,11 +145,7 @@ "posts": "Publicaciones", "events": "Eventos", "blockedUsers": "Usuarios bloqueados", - "membershipRequests": "Solicitudes de membresía", - "deleteOrganization": "Eliminar Organización", - "deleteMsg": "¿Desea eliminar esta organización?", - "no": "No", - "yes": "Sí", + "requests": "Solicitudes", "talawaApiUnavailable": "El servicio Talawa-API no está disponible. ¿Está funcionando? Compruebe también la conectividad de su red." }, "organizationPeople": { @@ -297,6 +293,7 @@ }, "blockUnblockUser": { "title": "Usuario de bloqueo/desbloqueo de Talawa", + "pageName": "Bloqueo/desbloqueo", "searchByName": "Buscar por nombre", "listOfUsers": "Lista de Usuarios que enviaron spam", "name": "Nombre", @@ -310,8 +307,10 @@ "talawaApiUnavailable": "El servicio Talawa-API no está disponible. ¿Está funcionando? Compruebe también la conectividad de su red.", "allMembers": "Todos los miembros", "blockedUsers": "Usuarios bloqueados", - "searchFirstName": "Ingrese el nombre", - "searchLastName": "Introduzca el apellido" + "searchByFirstName": "Buscar por nombre de pila", + "searchByLastName": "Buscar por apellido", + "noResultsFoundFor": "No se encontraron resultados para ", + "noSpammerFound": "No se encontró ningún spammer" }, "forgotPassword": { "title": "Talawa olvidó su contraseña", @@ -360,13 +359,22 @@ }, "orgSettings": { "title": "Configuración Talawa", + "pageName": "Configuración", "updateYourDetails": "Actualiza tus datos", "updateYourPassword": "Actualice su contraseña", "updateOrganization": "Actualizar Organización", - "deleteOrganization": "Eliminar Organización", "seeRequest": "Ver Solicitud", "settings": "Ajustes", - "noData": "Sin datos" + "noData": "Sin datos", + "otherSettings": "Otras Configuraciones", + "changeLanguage": "Cambiar Idioma" + }, + "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." }, "userUpdate": { "firstName": "Primer nombre", @@ -403,8 +411,8 @@ "description": "Descripción", "location": "ubicación", "displayImage": "Mostrar imagen", - "isPublic": "Es público", - "isRegistrable": "Es registrable", + "isPublic": "Público", + "isVisibleInSearch": "Visible en la búsqueda", "saveChanges": "Guardar cambios", "cancel": "Cancelar", "enterNameOrganization": "Ingrese el nombre de la organización", diff --git a/public/locales/zh.json b/public/locales/zh.json index e91c57c531..a4232d43cf 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -145,11 +145,7 @@ "posts": "帖子", "events": "事件", "blockedUsers": "被阻止的用戶", - "membershipRequests": "會員申請", - "deleteOrganization": "刪除組織", - "deleteMsg": "您要刪除此組織嗎?", - "no": "不", - "yes": "是的", + "requests": "请求", "talawaApiUnavailable": "服務不可用。它在運行嗎?還要檢查您的網絡連接。" }, "organizationPeople": { @@ -297,6 +293,7 @@ }, "blockUnblockUser": { "title": "塔拉瓦封鎖/解除封鎖用戶", + "pageName": "封锁/解封", "searchByName": "按名稱搜索", "listOfUsers": "發送垃圾郵件的用戶列表", "name": "姓名", @@ -310,8 +307,10 @@ "talawaApiUnavailable": "服務不可用。它在運行嗎?還要檢查您的網絡連接。", "allMembers": "所有成员", "blockedUsers": "被阻止的用户", - "searchFirstName": "输入名字", - "searchLastName": "输入姓氏" + "searchByFirstName": "按名字搜索", + "searchByLastName": "按姓氏搜索", + "noResultsFoundFor": "未找到结果 ", + "noSpammerFound": "未发现垃圾邮件发送者" }, "forgotPassword": { "title": "塔拉瓦忘記密碼", @@ -360,13 +359,22 @@ }, "orgSettings": { "title": "塔拉瓦設置", + "pageName": "设置", "updateYourDetails": "更新您的詳細信息", "updateYourPassword": "更新您的密碼", "updateOrganization": "更新組織", - "deleteOrganization": "刪除組織", "seeRequest": "查看請求", "settings": "設置", - "noData": "沒有數據" + "noData": "沒有數據", + "otherSettings": "其他设置", + "changeLanguage": "更改语言" + }, + "deleteOrg": { + "deleteOrganization": "删除组织", + "deleteMsg": "您是否要删除此组织?", + "no": "否", + "yes": "是", + "longDelOrgMsg": "点击删除组织按钮后,将永久删除该组织以及其活动、标签和所有相关数据。" }, "userUpdate": { "firstName": "名", @@ -403,8 +411,8 @@ "description": "描述", "location": "地點", "displayImage": "顯示圖像", - "isPublic": "是公開的", - "isRegistrable": "可註冊", + "isPublic": "公开", + "isVisibleInSearch": "在搜索中可见", "saveChanges": "保存更改", "cancel": "取消", "enterNameOrganization": "輸入組織名稱", diff --git a/src/GraphQl/Queries/Queries.ts b/src/GraphQl/Queries/Queries.ts index 1f47769e4c..6efae00ed5 100644 --- a/src/GraphQl/Queries/Queries.ts +++ b/src/GraphQl/Queries/Queries.ts @@ -183,6 +183,8 @@ export const ORGANIZATIONS_LIST = gql` name description location + isPublic + visibleInSearch members { _id firstName diff --git a/src/assets/svgs/admin.svg b/src/assets/svgs/admin.svg new file mode 100644 index 0000000000..8ee42f611d --- /dev/null +++ b/src/assets/svgs/admin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svgs/icons/angleRight.svg b/src/assets/svgs/angleRight.svg similarity index 100% rename from src/assets/svgs/icons/angleRight.svg rename to src/assets/svgs/angleRight.svg diff --git a/src/assets/svgs/icons/blockUser.svg b/src/assets/svgs/blockUser.svg similarity index 100% rename from src/assets/svgs/icons/blockUser.svg rename to src/assets/svgs/blockUser.svg diff --git a/src/assets/svgs/blockedUser.svg b/src/assets/svgs/blockedUser.svg new file mode 100644 index 0000000000..bbe0a51f84 --- /dev/null +++ b/src/assets/svgs/blockedUser.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/icons/dashboard.svg b/src/assets/svgs/dashboard.svg similarity index 100% rename from src/assets/svgs/icons/dashboard.svg rename to src/assets/svgs/dashboard.svg diff --git a/src/assets/svgs/event.svg b/src/assets/svgs/event.svg new file mode 100644 index 0000000000..3c73e7b04e --- /dev/null +++ b/src/assets/svgs/event.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/icons/events.svg b/src/assets/svgs/events.svg similarity index 100% rename from src/assets/svgs/icons/events.svg rename to src/assets/svgs/events.svg diff --git a/src/assets/svgs/icons/logout.svg b/src/assets/svgs/logout.svg similarity index 100% rename from src/assets/svgs/icons/logout.svg rename to src/assets/svgs/logout.svg diff --git a/src/assets/svgs/icons/organizations.svg b/src/assets/svgs/organizations.svg similarity index 100% rename from src/assets/svgs/icons/organizations.svg rename to src/assets/svgs/organizations.svg diff --git a/src/assets/svgs/icons/people.svg b/src/assets/svgs/people.svg similarity index 100% rename from src/assets/svgs/icons/people.svg rename to src/assets/svgs/people.svg diff --git a/src/assets/svgs/icons/plugins.svg b/src/assets/svgs/plugins.svg similarity index 100% rename from src/assets/svgs/icons/plugins.svg rename to src/assets/svgs/plugins.svg diff --git a/src/assets/svgs/post.svg b/src/assets/svgs/post.svg new file mode 100644 index 0000000000..34e468523b --- /dev/null +++ b/src/assets/svgs/post.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/icons/posts.svg b/src/assets/svgs/posts.svg similarity index 100% rename from src/assets/svgs/icons/posts.svg rename to src/assets/svgs/posts.svg diff --git a/src/assets/svgs/icons/requests.svg b/src/assets/svgs/requests.svg similarity index 100% rename from src/assets/svgs/icons/requests.svg rename to src/assets/svgs/requests.svg diff --git a/src/assets/svgs/icons/roles.svg b/src/assets/svgs/roles.svg similarity index 100% rename from src/assets/svgs/icons/roles.svg rename to src/assets/svgs/roles.svg diff --git a/src/assets/svgs/icons/settings.svg b/src/assets/svgs/settings.svg similarity index 100% rename from src/assets/svgs/icons/settings.svg rename to src/assets/svgs/settings.svg diff --git a/src/assets/svgs/icons/tags.svg b/src/assets/svgs/tags.svg similarity index 100% rename from src/assets/svgs/icons/tags.svg rename to src/assets/svgs/tags.svg diff --git a/src/assets/svgs/users.svg b/src/assets/svgs/users.svg new file mode 100644 index 0000000000..a1a474206d --- /dev/null +++ b/src/assets/svgs/users.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/ChangeLanguageDropdown/ChangeLanguageDropDown.tsx b/src/components/ChangeLanguageDropdown/ChangeLanguageDropDown.tsx index d80b8cb225..8f4fd945ed 100644 --- a/src/components/ChangeLanguageDropdown/ChangeLanguageDropDown.tsx +++ b/src/components/ChangeLanguageDropdown/ChangeLanguageDropDown.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Dropdown } from 'react-bootstrap'; import i18next from 'i18next'; -import styles from './ChangeLanguageDropdown.module.css'; import { languages } from 'utils/languages'; import cookies from 'js-cookie'; @@ -23,9 +22,7 @@ const ChangeLanguageDropDown = ( return ( ( => changeLanguage(language.code)} disabled={currentLanguageCode === language.code} data-testid={`change-language-btn-${language.code}`} diff --git a/src/components/ChangeLanguageDropdown/ChangeLanguageDropdown.module.css b/src/components/ChangeLanguageDropdown/ChangeLanguageDropdown.module.css deleted file mode 100644 index e72c604905..0000000000 --- a/src/components/ChangeLanguageDropdown/ChangeLanguageDropdown.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.parentContainer { - margin: 0 1rem; -} - -.dropdownItem { - font-size: 0.9rem; -} diff --git a/src/components/DeleteOrg/DeleteOrg.module.css b/src/components/DeleteOrg/DeleteOrg.module.css new file mode 100644 index 0000000000..2b15a2ac0c --- /dev/null +++ b/src/components/DeleteOrg/DeleteOrg.module.css @@ -0,0 +1,25 @@ +.settingsBody { + margin: 2.5rem 0; +} + +.cardHeader { + padding: 1.25rem 1rem 1rem 1rem; + border-bottom: 1px solid var(--bs-gray-200); + display: flex; + justify-content: space-between; + align-items: center; +} + +.cardHeader .cardTitle { + font-size: 1.2rem; + font-weight: 600; +} + +.cardBody { + min-height: 180px; +} + +.cardBody .textBox { + margin: 0 0 3rem 0; + color: var(--bs-secondary); +} diff --git a/src/components/DeleteOrg/DeleteOrg.test.tsx b/src/components/DeleteOrg/DeleteOrg.test.tsx new file mode 100644 index 0000000000..936cf44e03 --- /dev/null +++ b/src/components/DeleteOrg/DeleteOrg.test.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { MockedProvider } from '@apollo/react-testing'; +import { render, screen } from '@testing-library/react'; +import 'jest-location-mock'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; + +import { DELETE_ORGANIZATION_MUTATION } from 'GraphQl/Mutations/mutations'; +import { act } from 'react-dom/test-utils'; +import { store } from 'state/store'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import i18nForTest from 'utils/i18nForTest'; +import DeleteOrg from './DeleteOrg'; + +const MOCKS = [ + { + request: { + query: DELETE_ORGANIZATION_MUTATION, + variables: { + id: 123, + }, + }, + result: { + data: { + removeOrganization: [ + { + _id: 123, + }, + ], + }, + }, + }, +]; + +const link = new StaticMockLink(MOCKS, true); + +afterEach(() => { + localStorage.clear(); +}); + +describe('Delete Organization Component', () => { + test('should be able to Toggle Delete Organization Modal', async () => { + window.location.assign('/orgsetting/id=123'); + localStorage.setItem('UserType', 'SUPERADMIN'); + render( + + + + + + + + + + ); + screen.getByTestId(/openDeleteModalBtn/i).click(); + expect(screen.getByTestId(/orgDeleteModal/i)).toBeInTheDocument(); + screen.getByTestId(/closeDelOrgModalBtn/i).click(); + await act(async () => { + expect(screen.queryByTestId(/orgDeleteModal/i)).not.toHaveFocus(); + }); + expect(window.location).toBeAt('/orgsetting/id=123'); + }); + + test('Delete organization functionality should work properly', async () => { + window.location.assign('/orgsetting/id=123'); + localStorage.setItem('UserType', 'SUPERADMIN'); + render( + + + + + + + + + + ); + screen.getByTestId(/openDeleteModalBtn/i).click(); + screen.getByTestId(/deleteOrganizationBtn/i).click(); + expect(window.location).not.toBeNull(); + }); +}); diff --git a/src/components/DeleteOrg/DeleteOrg.tsx b/src/components/DeleteOrg/DeleteOrg.tsx new file mode 100644 index 0000000000..e6442d6558 --- /dev/null +++ b/src/components/DeleteOrg/DeleteOrg.tsx @@ -0,0 +1,89 @@ +import { useMutation } from '@apollo/client'; +import { DELETE_ORGANIZATION_MUTATION } from 'GraphQl/Mutations/mutations'; +import React, { useState } from 'react'; +import { Button, Card, Modal } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import { errorHandler } from 'utils/errorHandler'; +import styles from './DeleteOrg.module.css'; + +function deleteOrg(): JSX.Element { + const { t } = useTranslation('translation', { + keyPrefix: 'deleteOrg', + }); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const currentUrl = window.location.href.split('=')[1]; + const canDelete = localStorage.getItem('UserType') === 'SUPERADMIN'; + const toggleDeleteModal = (): void => setShowDeleteModal(!showDeleteModal); + const [del] = useMutation(DELETE_ORGANIZATION_MUTATION); + + const deleteOrg = async (): Promise => { + try { + const { data } = await del({ + variables: { + id: currentUrl, + }, + }); + /* istanbul ignore next */ + if (data) { + window.location.replace('/orglist'); + } + } catch (error: any) { + /* istanbul ignore next */ + errorHandler(t, error); + } + }; + + return ( + <> + {canDelete && ( + +
+
{t('deleteOrganization')}
+
+ +
{t('longDelOrgMsg')}
+ +
+
+ )} + {/* Delete Organization Modal */} + {canDelete && ( + + +
{t('deleteOrganization')}
+
+ {t('deleteMsg')} + + + + +
+ )} + + ); +} + +export default deleteOrg; diff --git a/src/components/IconComponent/IconComponent.tsx b/src/components/IconComponent/IconComponent.tsx index 5bbbf44334..a4648a6e03 100644 --- a/src/components/IconComponent/IconComponent.tsx +++ b/src/components/IconComponent/IconComponent.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { QuestionMarkOutlined } from '@mui/icons-material'; -import { ReactComponent as BlockUserIcon } from '../../assets/svgs/icons/blockUser.svg'; -import { ReactComponent as DashboardIcon } from '../../assets/svgs/icons/dashboard.svg'; -import { ReactComponent as EventsIcon } from '../../assets/svgs/icons/events.svg'; -import { ReactComponent as OrganizationsIcon } from '../../assets/svgs/icons/organizations.svg'; -import { ReactComponent as PeopleIcon } from '../../assets/svgs/icons/people.svg'; -import { ReactComponent as PluginsIcon } from '../../assets/svgs/icons/plugins.svg'; -import { ReactComponent as PostsIcon } from '../../assets/svgs/icons/posts.svg'; -import { ReactComponent as SettingsIcon } from '../../assets/svgs/icons/settings.svg'; +import { ReactComponent as BlockUserIcon } from 'assets/svgs/blockUser.svg'; +import { ReactComponent as DashboardIcon } from 'assets/svgs/dashboard.svg'; +import { ReactComponent as EventsIcon } from 'assets/svgs/events.svg'; +import { ReactComponent as OrganizationsIcon } from 'assets/svgs/organizations.svg'; +import { ReactComponent as PeopleIcon } from 'assets/svgs/people.svg'; +import { ReactComponent as PluginsIcon } from 'assets/svgs/plugins.svg'; +import { ReactComponent as PostsIcon } from 'assets/svgs/posts.svg'; +import { ReactComponent as SettingsIcon } from 'assets/svgs/settings.svg'; export interface InterfaceIconComponent { name: string; diff --git a/src/components/LeftDrawer/LeftDrawer.tsx b/src/components/LeftDrawer/LeftDrawer.tsx index 87e37097e7..e28f40d31a 100644 --- a/src/components/LeftDrawer/LeftDrawer.tsx +++ b/src/components/LeftDrawer/LeftDrawer.tsx @@ -3,12 +3,12 @@ import Button from 'react-bootstrap/Button'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; import { toast } from 'react-toastify'; -import { ReactComponent as AngleRightIcon } from '../../assets/svgs/icons/angleRight.svg'; -import { ReactComponent as LogoutIcon } from '../../assets/svgs/icons/logout.svg'; -import { ReactComponent as OrganizationsIcon } from '../../assets/svgs/icons/organizations.svg'; -import { ReactComponent as RequestsIcon } from '../../assets/svgs/icons/requests.svg'; -import { ReactComponent as RolesIcon } from '../../assets/svgs/icons/roles.svg'; -import { ReactComponent as TalawaLogo } from '../../assets/svgs/talawa.svg'; +import { ReactComponent as AngleRightIcon } from 'assets/svgs/angleRight.svg'; +import { ReactComponent as LogoutIcon } from 'assets/svgs/logout.svg'; +import { ReactComponent as OrganizationsIcon } from 'assets/svgs/organizations.svg'; +import { ReactComponent as RequestsIcon } from 'assets/svgs/requests.svg'; +import { ReactComponent as RolesIcon } from 'assets/svgs/roles.svg'; +import { ReactComponent as TalawaLogo } from 'assets/svgs/talawa.svg'; import styles from './LeftDrawer.module.css'; export interface InterfaceLeftDrawerProps { diff --git a/src/components/LeftDrawerOrg/LeftDrawerOrg.test.tsx b/src/components/LeftDrawerOrg/LeftDrawerOrg.test.tsx index 0c108533cc..ff10c1b28b 100644 --- a/src/components/LeftDrawerOrg/LeftDrawerOrg.test.tsx +++ b/src/components/LeftDrawerOrg/LeftDrawerOrg.test.tsx @@ -83,6 +83,8 @@ const MOCKS = [ name: 'Test Organization', description: 'Testing this organization', location: 'Gotham, DC', + isPublic: true, + visibleInSearch: true, members: [ { _id: 'john123', @@ -135,6 +137,8 @@ const MOCKS_WITH_IMAGE = [ name: 'Test Organization', description: 'Testing this organization', location: 'Gotham, DC', + isPublic: true, + visibleInSearch: true, members: [ { _id: 'john123', @@ -261,11 +265,7 @@ describe('Testing Left Drawer component for SUPERADMIN', () => { ); await wait(); - // Coming soon - userEvent.click(screen.getByTestId(/orgBtn/i)); - expect(toast.success).toHaveBeenCalledWith( - 'Organization detail modal coming soon!' - ); + expect(screen.getByTestId(/orgBtn/i)).toBeInTheDocument(); userEvent.click(screen.getByTestId(/profileBtn/i)); expect(toast.success).toHaveBeenCalledWith('Profile page coming soon!'); }); diff --git a/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx b/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx index 5ec1f615bd..119a90f0eb 100644 --- a/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx +++ b/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx @@ -3,16 +3,16 @@ import { WarningAmberOutlined } from '@mui/icons-material'; import { ORGANIZATIONS_LIST } from 'GraphQl/Queries/Queries'; import CollapsibleDropdown from 'components/CollapsibleDropdown/CollapsibleDropdown'; import IconComponent from 'components/IconComponent/IconComponent'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import Button from 'react-bootstrap/Button'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; import { toast } from 'react-toastify'; import type { TargetsType } from 'state/reducers/routesReducer'; import type { InterfaceQueryOrganizationsListObject } from 'utils/interfaces'; -import { ReactComponent as AngleRightIcon } from '../../assets/svgs/icons/angleRight.svg'; -import { ReactComponent as LogoutIcon } from '../../assets/svgs/icons/logout.svg'; -import { ReactComponent as TalawaLogo } from '../../assets/svgs/talawa.svg'; +import { ReactComponent as AngleRightIcon } from 'assets/svgs/angleRight.svg'; +import { ReactComponent as LogoutIcon } from 'assets/svgs/logout.svg'; +import { ReactComponent as TalawaLogo } from 'assets/svgs/talawa.svg'; import styles from './LeftDrawerOrg.module.css'; export interface InterfaceLeftDrawerProps { @@ -31,7 +31,8 @@ const leftDrawerOrg = ({ setHideDrawer, }: InterfaceLeftDrawerProps): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'leftDrawerOrg' }); - + const [organization, setOrganization] = + useState(); const { data, loading, @@ -51,6 +52,17 @@ const leftDrawerOrg = ({ const history = useHistory(); + // Set organization data + useEffect(() => { + let isMounted = true; + if (data && isMounted) { + setOrganization(data?.organizations[0]); + } + return () => { + isMounted = false; + }; + }, [data]); + const logout = (): void => { localStorage.clear(); history.push('/'); @@ -95,7 +107,7 @@ const leftDrawerOrg = ({ data-testid="orgBtn" /> - ) : data && data?.organizations.length == 0 ? ( + ) : organization == undefined ? ( <> )} diff --git a/src/components/Loader/Loader.module.css b/src/components/Loader/Loader.module.css index df8c1deea6..aad512e826 100644 --- a/src/components/Loader/Loader.module.css +++ b/src/components/Loader/Loader.module.css @@ -6,8 +6,20 @@ align-items: center; } -.spinner { +.spinnerXl { width: 6rem; height: 6rem; border-width: 0.5rem; } + +.spinnerLg { + height: 4rem; + width: 4rem; + border-width: 0.3rem; +} + +.spinnerSm { + height: 2rem; + width: 2rem; + border-width: 0.2rem; +} diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx index 6dc247b4c9..f761ebd79b 100644 --- a/src/components/Loader/Loader.tsx +++ b/src/components/Loader/Loader.tsx @@ -2,12 +2,28 @@ import React from 'react'; import styles from './Loader.module.css'; import { Spinner } from 'react-bootstrap'; -const Loader = (): JSX.Element => { +interface InterfaceLoaderProps { + styles?: StyleSheet | string; + size?: 'sm' | 'lg' | 'xl'; +} + +const Loader = (props: InterfaceLoaderProps): JSX.Element => { return ( <> -
+
div { - width: 50%; - margin-right: 50px; + justify-content: center; + align-items: center; + flex-direction: column; } -.radio_buttons > input { - margin-bottom: 20px; - border: none; - box-shadow: none; - padding: 0 0; - border-radius: 5px; - background: none; - width: 50%; -} - -.whitebtn { - margin: 1rem 0 0; - margin-top: 10px; - border: 1px solid #e8e5e5; - box-shadow: 0 2px 2px #e8e5e5; - padding: 10px 20px; - border-radius: 5px; - background: none; - width: 20%; - font-size: 16px; - color: #31bb6b; - outline: none; - font-weight: 600; - cursor: pointer; - float: left; - transition: transform 0.2s, box-shadow 0.2s; -} -.greenregbtn { - margin: 1rem 0 0; - margin-top: 10px; - margin-right: 30px; - border: 1px solid #e8e5e5; - box-shadow: 0 2px 2px #e8e5e5; - padding: 10px 10px; - border-radius: 5px; - background-color: #31bb6b; - width: 20%; - font-size: 16px; - color: white; - outline: none; - font-weight: 600; - cursor: pointer; - transition: transform 0.2s, box-shadow 0.2s; -} -.radio_buttons { - width: 55%; - margin-top: 10px; - display: flex; - color: #707070; - font-weight: 600; - font-size: 14px; -} -.radio_buttons > input { - transform: scale(1.2); -} -.radio_buttons > label { - margin-top: -4px; - margin-left: 0px; - margin-right: 7px; -} -.idtitle { - width: 88%; -} -.checkboxdiv { - display: flex; - width: 100%; - margin-top: 20px; -} -.checkboxdiv > div { - display: flex; - width: 50%; -} -.checkboxdiv > div > input { - width: 30%; - border: none; - box-shadow: none; - margin-top: 5px; +.icon { + transform: scale(1.5); + color: var(--bs-danger); + margin-bottom: 1rem; } diff --git a/src/components/OrgUpdate/OrgUpdate.test.tsx b/src/components/OrgUpdate/OrgUpdate.test.tsx index 187e736c69..79b076c452 100644 --- a/src/components/OrgUpdate/OrgUpdate.test.tsx +++ b/src/components/OrgUpdate/OrgUpdate.test.tsx @@ -1,109 +1,19 @@ import React from 'react'; -import { act, render, screen } from '@testing-library/react'; import { MockedProvider } from '@apollo/react-testing'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { I18nextProvider } from 'react-i18next'; -import OrgUpdate from './OrgUpdate'; -import { UPDATE_ORGANIZATION_MUTATION } from 'GraphQl/Mutations/mutations'; -import i18nForTest from 'utils/i18nForTest'; -import { ORGANIZATIONS_LIST } from 'GraphQl/Queries/Queries'; import { StaticMockLink } from 'utils/StaticMockLink'; +import i18nForTest from 'utils/i18nForTest'; +import OrgUpdate from './OrgUpdate'; +import { + MOCKS, + MOCKS_ERROR_ORGLIST, + MOCKS_ERROR_UPDATE_ORGLIST, +} from './OrgUpdateMocks'; -const MOCKS = [ - { - request: { - query: ORGANIZATIONS_LIST, - }, - result: { - data: { - organizations: [ - { - _id: '123', - image: '', - name: '', - description: '', - creator: { - firstName: '', - lastName: '', - email: '', - }, - location: '', - members: { - _id: '123', - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - }, - admins: { - _id: '123', - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - }, - membershipRequests: { - _id: '456', - user: { - firstName: 'Sam', - lastName: 'Smith', - email: 'samsmith@gmail.com', - }, - }, - blockedUsers: { - _id: '789', - firstName: 'Steve', - lastName: 'Smith', - email: 'stevesmith@gmail.com', - }, - tags: ['Shelter', 'NGO', 'Open Source'], - spamCount: [ - { - _id: '6954', - user: { - _id: '878', - firstName: 'Joe', - lastName: 'Root', - email: 'joeroot@gmail.com', - }, - isReaded: false, - groupchat: { - _id: '321', - title: 'Dummy', - }, - }, - ], - }, - ], - }, - }, - }, - { - request: { - query: UPDATE_ORGANIZATION_MUTATION, - variables: { - id: '123', - name: 'Updated Organization', - description: 'This is an updated test organization', - location: 'Updated location', - image: new File(['hello'], 'hello.png', { type: 'image/png' }), - isPublic: true, - visibleInSearch: false, - }, - }, - result: { - data: { - updateOrganization: { - _id: '123', - name: 'Updated Organization', - description: 'This is an updated test organization', - location: 'Updated location', - isPublic: true, - visibleInSearch: false, - }, - }, - }, - }, -]; const link = new StaticMockLink(MOCKS, true); + async function wait(ms = 500): Promise { await act(() => { return new Promise((resolve) => { @@ -114,24 +24,22 @@ async function wait(ms = 500): Promise { describe('Testing Organization Update', () => { const props = { - id: '123', - orgid: '123', + orgId: '123', }; const formData = { - name: 'John Doe', - description: 'This is a description', - location: 'Test location', + name: 'Palisadoes Organization', + description: 'This is a updated description', + location: 'This is updated location', displayImage: new File(['hello'], 'hello.png', { type: 'image/png' }), - isPublic: true, + isPublic: false, isVisible: true, }; global.alert = jest.fn(); - test('should render props and text elements test for the page component', async () => { - //window.location.assign('/orgsetting/id=123'); - await act(async () => { + test('should render props and text elements test for the page component along with mock data', async () => { + act(() => { render( @@ -139,50 +47,128 @@ describe('Testing Organization Update', () => { ); - await wait(); - userEvent.type( - screen.getByPlaceholderText(/Enter Organization Name/i), - formData.name - ); - userEvent.type( - screen.getByPlaceholderText(/Description/i), - formData.description - ); - userEvent.type( - screen.getByPlaceholderText(/Location/i), - formData.location - ); - userEvent.upload( - screen.getByLabelText(/Display Image:/i), - formData.displayImage - ); - userEvent.click(screen.getByLabelText(/Is Public:/i)); - userEvent.click(screen.getByLabelText(/Is Registrable:/i)); + }); + await wait(); + // Check labels are present or not + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Location')).toBeInTheDocument(); + expect(screen.getByText('Display Image:')).toBeInTheDocument(); + expect(screen.getByText('Public:')).toBeInTheDocument(); + expect(screen.getByText('Visible in Search:')).toBeInTheDocument(); - await wait(); + // Get the input fields, and btns + const name = screen.getByPlaceholderText(/Enter Organization Name/i); + const des = screen.getByPlaceholderText(/Description/i); + const location = screen.getByPlaceholderText(/Location/i); + const isPublic = screen.getByPlaceholderText(/Public/i); + const isVisible = screen.getByPlaceholderText(/Visible/i); - userEvent.click(screen.getByText(/Save Changes/i)); + // Checking if form fields got updated according to the mock data + expect(name).toHaveValue('Palisadoes'); + expect(des).toHaveValue('Equitable Access to STEM Education Jobs'); + expect(location).toHaveValue('Jamaica'); + expect(isPublic).toBeChecked(); + expect(isVisible).not.toBeChecked(); + }); - expect(screen.getByPlaceholderText(/Organization Name/i)).toHaveValue( - formData.name + test('Should Update organization properly', async () => { + await act(async () => { + render( + + + + + ); - expect(screen.getByPlaceholderText(/Description/i)).toHaveValue( - formData.description + }); + + await wait(); + + // Get the input fields, and btns + const name = screen.getByPlaceholderText(/Enter Organization Name/i); + const des = screen.getByPlaceholderText(/Description/i); + const location = screen.getByPlaceholderText(/Location/i); + const displayImage = screen.getByPlaceholderText(/Display Image/i); + const isPublic = screen.getByPlaceholderText(/Public/i); + const isVisible = screen.getByPlaceholderText(/Visible/i); + const saveChangesBtn = screen.getByText(/Save Changes/i); + + // Emptying the text fields to add updated data + fireEvent.change(name, { target: { value: '' } }); + fireEvent.change(des, { target: { value: '' } }); + fireEvent.change(location, { target: { value: '' } }); + + // Mocking filling form behaviour + userEvent.type(name, formData.name); + userEvent.type(des, formData.description); + userEvent.type(location, formData.location); + userEvent.upload(displayImage, formData.displayImage); + userEvent.click(isPublic); + userEvent.click(isVisible); + + await wait(); + userEvent.click(saveChangesBtn); + + // Checking if the form got update accordingly + expect(name).toHaveValue(formData.name); + expect(des).toHaveValue(formData.description); + expect(location).toHaveValue(formData.location); + expect(displayImage).toBeTruthy(); + expect(isPublic).not.toBeChecked(); + expect(isVisible).toBeChecked(); + }); + + test('Should render error occured text when Organization Could not be found', async () => { + act(() => { + render( + + + + + ); - expect(screen.getByPlaceholderText(/Location/i)).toHaveValue( - formData.location + }); + await wait(); + expect(screen.getByText(/Mock Graphql Error/i)).toBeInTheDocument(); + }); + + test('Should show error occured toast when Organization could not be updated', async () => { + await act(async () => { + render( + + + + + ); - expect(screen.getByLabelText(/display image:/i)).toBeTruthy(); - expect(screen.getByLabelText(/Is Public:/i)).not.toBeChecked(); - expect(screen.getByLabelText(/Is Registrable:/i)).toBeChecked(); - expect(screen.getByText(/Cancel/i)).toBeTruthy(); - - expect(screen.getByText('Name')).toBeInTheDocument(); - expect(screen.getByText('Description')).toBeInTheDocument(); - expect(screen.getByText('Location')).toBeInTheDocument(); - expect(screen.getByText('Display Image:')).toBeInTheDocument(); - expect(screen.getByText('Is Public:')).toBeInTheDocument(); - expect(screen.getByText('Is Registrable:')).toBeInTheDocument(); }); + + await wait(); + + // Get the input fields, and btns + const name = screen.getByPlaceholderText(/Enter Organization Name/i); + const des = screen.getByPlaceholderText(/Description/i); + const location = screen.getByPlaceholderText(/Location/i); + const displayImage = screen.getByPlaceholderText(/Display Image/i); + const isPublic = screen.getByPlaceholderText(/Public/i); + const isVisible = screen.getByPlaceholderText(/Visible/i); + const saveChangesBtn = screen.getByText(/Save Changes/i); + + // Emptying the text fields to add updated data + fireEvent.change(name, { target: { value: '' } }); + fireEvent.change(des, { target: { value: '' } }); + fireEvent.change(location, { target: { value: '' } }); + + // Mocking filling form behaviour + userEvent.type(name, formData.name); + userEvent.type(des, formData.description); + userEvent.type(location, formData.location); + userEvent.upload(displayImage, formData.displayImage); + userEvent.click(isPublic); + userEvent.click(isVisible); + + await wait(); + userEvent.click(saveChangesBtn); }); }); diff --git a/src/components/OrgUpdate/OrgUpdate.tsx b/src/components/OrgUpdate/OrgUpdate.tsx index b93d1ff218..16222717d6 100644 --- a/src/components/OrgUpdate/OrgUpdate.tsx +++ b/src/components/OrgUpdate/OrgUpdate.tsx @@ -1,26 +1,28 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { useMutation, useQuery } from '@apollo/client'; -import { useTranslation } from 'react-i18next'; import Button from 'react-bootstrap/Button'; +import { useTranslation } from 'react-i18next'; import { toast } from 'react-toastify'; +import type { ApolloError } from '@apollo/client'; +import { WarningAmberRounded } from '@mui/icons-material'; import { UPDATE_ORGANIZATION_MUTATION } from 'GraphQl/Mutations/mutations'; -import styles from './OrgUpdate.module.css'; import { ORGANIZATIONS_LIST } from 'GraphQl/Queries/Queries'; +import Loader from 'components/Loader/Loader'; +import { Col, Form, Row } from 'react-bootstrap'; import convertToBase64 from 'utils/convertToBase64'; import { errorHandler } from 'utils/errorHandler'; -import { Form } from 'react-bootstrap'; +import type { InterfaceQueryOrganizationsListObject } from 'utils/interfaces'; +import styles from './OrgUpdate.module.css'; interface InterfaceOrgUpdateProps { - id: string; - orgid: string; + orgId: string; } -// eslint-disable-next-line @typescript-eslint/no-unused-vars function orgUpdate(props: InterfaceOrgUpdateProps): JSX.Element { - const currentUrl = window.location.href.split('=')[1]; + const { orgId } = props; - const [formState, setFormState] = React.useState<{ + const [formState, setFormState] = useState<{ orgName: string; orgDescrip: string; location: string; @@ -32,7 +34,7 @@ function orgUpdate(props: InterfaceOrgUpdateProps): JSX.Element { orgImage: null, }); - const [publicchecked, setPublicChecked] = React.useState(true); + const [publicchecked, setPublicChecked] = React.useState(false); const [visiblechecked, setVisibleChecked] = React.useState(false); const [login] = useMutation(UPDATE_ORGANIZATION_MUTATION); @@ -41,30 +43,45 @@ function orgUpdate(props: InterfaceOrgUpdateProps): JSX.Element { keyPrefix: 'orgUpdate', }); - const { data, loading: loadingdata } = useQuery(ORGANIZATIONS_LIST, { - variables: { id: currentUrl }, + const { + data, + loading, + refetch, + error, + }: { + data?: { + organizations: InterfaceQueryOrganizationsListObject[]; + }; + loading: boolean; + refetch: (variables: { id: string }) => void; + error?: ApolloError; + } = useQuery(ORGANIZATIONS_LIST, { + variables: { id: orgId }, + notifyOnNetworkStatusChange: true, }); - React.useEffect(() => { - if (data) { + useEffect(() => { + let isMounted = true; + if (data && isMounted) { setFormState({ ...formState, orgName: data.organizations[0].name, orgDescrip: data.organizations[0].description, location: data.organizations[0].location, }); + setPublicChecked(data.organizations[0].isPublic); + setVisibleChecked(data.organizations[0].visibleInSearch); } - }, [data]); - - if (loadingdata) { - return
; - } + return () => { + isMounted = false; + }; + }, [data, orgId]); const onSaveChangesClicked = async (): Promise => { try { const { data } = await login({ variables: { - id: currentUrl, + id: orgId, name: formState.orgName, description: formState.orgDescrip, location: formState.location, @@ -73,144 +90,127 @@ function orgUpdate(props: InterfaceOrgUpdateProps): JSX.Element { file: formState.orgImage, }, }); - /* istanbul ignore next */ + // istanbul ignore next if (data) { - window.location.assign(`/orgdash/id=${props.orgid}`); - + refetch({ id: orgId }); toast.success(t('successfulUpdated')); } } catch (error: any) { - /* istanbul ignore next */ errorHandler(t, error); } }; - /* istanbul ignore next */ - const cancelUpdate = (): void => { - window.location.reload(); - }; + if (loading) { + return ; + } + + if (error) { + return ( +
+ +
+ Error occured while loading Organization Data +
+ {`${error.message}`} +
+
+ ); + } return ( <>
- {/*

Update Your Details

*/} -
-
- - { - setFormState({ - ...formState, - orgName: e.target.value, - }); - }} + {t('name')} + { + setFormState({ + ...formState, + orgName: e.target.value, + }); + }} + /> + {t('description')} + { + setFormState({ + ...formState, + orgDescrip: e.target.value, + }); + }} + /> + {t('location')} + { + setFormState({ + ...formState, + location: e.target.value, + }); + }} + /> + + + {t('isPublic')}: + setPublicChecked(!publicchecked)} /> -
-
- - { - setFormState({ - ...formState, - orgDescrip: e.target.value, - }); - }} + + + + {t('isVisibleInSearch')}: + + setVisibleChecked(!visiblechecked)} /> -
-
-
-
- - { - setFormState({ - ...formState, - location: e.target.value, - }); - }} - /> -
-
-
-
- -
-
-
- - setPublicChecked(!publicchecked)} - /> -
-
- - setVisibleChecked(!visiblechecked)} - /> -
-
-
-
+ + + {t('displayImage')}: + => { + const target = e.target as HTMLInputElement; + const file = target.files && target.files[0]; + /* istanbul ignore else */ + if (file) + setFormState({ + ...formState, + orgImage: await convertToBase64(file), + }); + }} + data-testid="organisationImage" + /> +
-
diff --git a/src/components/OrgUpdate/OrgUpdateMocks.ts b/src/components/OrgUpdate/OrgUpdateMocks.ts new file mode 100644 index 0000000000..cd78d37fd0 --- /dev/null +++ b/src/components/OrgUpdate/OrgUpdateMocks.ts @@ -0,0 +1,157 @@ +import { UPDATE_ORGANIZATION_MUTATION } from 'GraphQl/Mutations/mutations'; +import { ORGANIZATIONS_LIST } from 'GraphQl/Queries/Queries'; + +export const MOCKS = [ + { + request: { + query: ORGANIZATIONS_LIST, + variables: { id: '123' }, + }, + result: { + data: { + organizations: [ + { + _id: '123', + image: null, + name: 'Palisadoes', + description: 'Equitable Access to STEM Education Jobs', + location: 'Jamaica', + isPublic: true, + visibleInSearch: false, + creator: { + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@example.com', + }, + members: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', + }, + admins: [ + { + _id: '123', + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', + }, + ], + membershipRequests: { + _id: '456', + user: { + firstName: 'Sam', + lastName: 'Smith', + email: 'samsmith@gmail.com', + }, + }, + blockedUsers: [], + }, + ], + }, + }, + }, + { + request: { + query: UPDATE_ORGANIZATION_MUTATION, + variables: { + id: '123', + name: 'Updated Organization', + description: 'This is an updated test organization', + location: 'Updated location', + image: new File(['hello'], 'hello.png', { type: 'image/png' }), + isPublic: true, + visibleInSearch: false, + }, + }, + result: { + data: { + updateOrganization: { + _id: '123', + name: 'Updated Organization', + description: 'This is an updated test organization', + location: 'Updated location', + isPublic: true, + visibleInSearch: false, + }, + }, + }, + }, +]; + +export const MOCKS_ERROR_ORGLIST = [ + { + request: { + query: ORGANIZATIONS_LIST, + variables: { id: '123' }, + }, + error: new Error('Mock Graphql Error'), + }, +]; + +export const MOCKS_ERROR_UPDATE_ORGLIST = [ + { + request: { + query: ORGANIZATIONS_LIST, + variables: { id: '123' }, + }, + result: { + data: { + organizations: [ + { + _id: '123', + image: null, + name: 'Palisadoes', + description: 'Equitable Access to STEM Education Jobs', + location: 'Jamaica', + isPublic: true, + visibleInSearch: false, + creator: { + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@example.com', + }, + members: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', + }, + admins: [ + { + _id: '123', + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', + }, + ], + membershipRequests: { + _id: '456', + user: { + firstName: 'Sam', + lastName: 'Smith', + email: 'samsmith@gmail.com', + }, + }, + blockedUsers: [], + }, + ], + }, + }, + }, + { + request: { + query: UPDATE_ORGANIZATION_MUTATION, + variables: { + id: '123', + name: 'Updated Organization', + description: 'This is an updated test organization', + location: 'Updated location', + image: new File(['hello'], 'hello.png', { type: 'image/png' }), + isPublic: true, + visibleInSearch: false, + }, + }, + erorr: new Error('Mock Graphql Updating Organization Error'), + }, +]; diff --git a/src/components/OrganizationDashCards/CardItem.module.css b/src/components/OrganizationDashCards/CardItem.module.css new file mode 100644 index 0000000000..e90a6d3655 --- /dev/null +++ b/src/components/OrganizationDashCards/CardItem.module.css @@ -0,0 +1,47 @@ +.cardItem { + position: relative; + display: flex; + align-items: center; + padding: 0.75rem 0; +} + +.cardItem .iconWrapper { + position: relative; + height: 40px; + width: 40px; + display: flex; + justify-content: center; + align-items: center; +} + +.cardItem .iconWrapper .themeOverlay { + background: var(--bs-primary); + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.12; + border-radius: 50%; +} + +.cardItem .iconWrapper .dangerOverlay { + background: var(--bs-danger); + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.12; + border-radius: 50%; +} + +.cardItem .title { + font-size: 1rem; + flex: 1; +} + +.cardItem .time { + font-size: 0.9rem; + color: var(--bs-secondary); +} diff --git a/src/components/OrganizationDashCards/CardItem.test.tsx b/src/components/OrganizationDashCards/CardItem.test.tsx new file mode 100644 index 0000000000..6841fe9659 --- /dev/null +++ b/src/components/OrganizationDashCards/CardItem.test.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import CardItem from './CardItem'; +import type { InterfaceCardItem } from './CardItem'; + +describe('Testing the Organization Card', () => { + test('should render props and text elements For event card', () => { + const props: InterfaceCardItem = { + type: 'Event', + title: 'Event Title', + time: '2023-09-03', + }; + + render(); + + expect(screen.getByText(/Event Title/i)).toBeInTheDocument(); + expect(screen.getByText(/03-09-2023/i)).toBeInTheDocument(); + }); + + test('Should render props and text elements for Post card', () => { + const props: InterfaceCardItem = { + type: 'Post', + title: 'Post Title', + time: '2023-09-03', + }; + + render(); + + expect(screen.getByText(/Post Title/i)).toBeInTheDocument(); + expect(screen.getByText(/03-09-2023/i)).toBeInTheDocument(); + }); + + test('Should render props and text elements for Membership Request card', () => { + const props: InterfaceCardItem = { + type: 'MembershipRequest', + title: 'Membership Request Title', + }; + + render(); + expect(screen.getByText(/Membership Request Title/i)).toBeInTheDocument(); + }); +}); diff --git a/src/components/OrganizationDashCards/CardItem.tsx b/src/components/OrganizationDashCards/CardItem.tsx new file mode 100644 index 0000000000..4843d421c5 --- /dev/null +++ b/src/components/OrganizationDashCards/CardItem.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { ReactComponent as EventsIcon } from 'assets/svgs/events.svg'; +import { ReactComponent as PostsIcon } from 'assets/svgs/post.svg'; +import dayjs from 'dayjs'; +import styles from './CardItem.module.css'; +import { PersonAddAlt1Rounded } from '@mui/icons-material'; + +export interface InterfaceCardItem { + type: 'Event' | 'Post' | 'MembershipRequest'; + title: string; + time?: string; +} + +const cardItem = (props: InterfaceCardItem): JSX.Element => { + const { type, title, time } = props; + return ( + <> +
+
+
+ {type == 'Event' ? ( + + ) : type == 'Post' ? ( + + ) : ( + type == 'MembershipRequest' && ( + + ) + )} +
+ {`${title}`} + {time ? ( + + {dayjs(time).format('DD-MM-YYYY')} + + ) : ( + '' + )} +
+ + ); +}; + +export default cardItem; diff --git a/src/components/OrganizationDashCards/CardItemLoading.tsx b/src/components/OrganizationDashCards/CardItemLoading.tsx new file mode 100644 index 0000000000..923128c2f2 --- /dev/null +++ b/src/components/OrganizationDashCards/CardItemLoading.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import styles from './CardItem.module.css'; + +const cardItemLoading = (): JSX.Element => { + return ( + <> +
+
+
+
+ +   + +
+ + ); +}; + +export default cardItemLoading; diff --git a/src/components/OrganizationDashCards/DashboardCard.test.tsx b/src/components/OrganizationDashCards/DashboardCard.test.tsx new file mode 100644 index 0000000000..71e5e1fed0 --- /dev/null +++ b/src/components/OrganizationDashCards/DashboardCard.test.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import DashboardCard from './DashboardCard'; + +describe('Testing the Dashboard Card', () => { + test('should render props and text elements For event card', () => { + const props = { + icon: , + title: 'Example Title', + count: 100, + }; + + render(); + + expect(screen.getByText(/Example Title/i)).toBeInTheDocument(); + expect(screen.getByText(/100/i)).toBeInTheDocument(); + }); +}); diff --git a/src/components/OrganizationDashCards/DashboardCard.tsx b/src/components/OrganizationDashCards/DashboardCard.tsx new file mode 100644 index 0000000000..4ad8fe8849 --- /dev/null +++ b/src/components/OrganizationDashCards/DashboardCard.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Card, Row } from 'react-bootstrap'; +import Col from 'react-bootstrap/Col'; +import styles from './Dashboardcard.module.css'; + +const dashBoardCard = (props: { + icon: React.ReactNode; + title: string; + count?: number; +}): JSX.Element => { + const { icon, count, title } = props; + return ( + + + + +
+
+ {icon} +
+ + + {count ?? 0} + {title} + + + + + ); +}; + +export default dashBoardCard; diff --git a/src/components/OrganizationDashCards/DashboardCardLoading.tsx b/src/components/OrganizationDashCards/DashboardCardLoading.tsx new file mode 100644 index 0000000000..5b596f32b2 --- /dev/null +++ b/src/components/OrganizationDashCards/DashboardCardLoading.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Card, Row } from 'react-bootstrap'; +import Col from 'react-bootstrap/Col'; +import styles from './Dashboardcard.module.css'; + +const dashBoardCardLoading = (): JSX.Element => { + return ( + + + + +
+
+
+ + + + + + + + + ); +}; + +export default dashBoardCardLoading; diff --git a/src/components/OrganizationDashCards/Dashboardcard.module.css b/src/components/OrganizationDashCards/Dashboardcard.module.css new file mode 100644 index 0000000000..365657fb4f --- /dev/null +++ b/src/components/OrganizationDashCards/Dashboardcard.module.css @@ -0,0 +1,60 @@ +.cardBody { + padding: 1.25rem 1.5rem; +} + +.cardBody .iconWrapper { + position: relative; + height: 48px; + width: 48px; + display: flex; + justify-content: center; + align-items: center; +} + +.cardBody .iconWrapper .themeOverlay { + background: var(--bs-primary); + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0.12; + border-radius: 50%; +} + +.cardBody .textWrapper .primaryText { + font-size: 24px; + font-weight: bold; + display: block; +} + +.cardBody .textWrapper .secondaryText { + font-size: 14px; + display: block; + color: var(--bs-secondary); +} + +@media (max-width: 600px) { + .cardBody { + min-height: 120px; + } + + .cardBody .iconWrapper { + position: absolute; + top: 1rem; + left: 1rem; + } + + .cardBody .textWrapper { + margin-top: calc(0.5rem + 36px); + text-align: right; + } + + .cardBody .textWrapper .primaryText { + font-size: 1.5rem; + } + + .cardBody .textWrapper .secondaryText { + font-size: 1rem; + } +} diff --git a/src/screens/BlockUser/BlockUser.module.css b/src/screens/BlockUser/BlockUser.module.css index 365774b438..ed93446206 100644 --- a/src/screens/BlockUser/BlockUser.module.css +++ b/src/screens/BlockUser/BlockUser.module.css @@ -1,184 +1,102 @@ -.mainpage { +.btnsContainer { display: flex; - flex-direction: row; + margin: 2.5rem 0 2.5rem 0; } -.sidebar { - z-index: 0; - padding-top: 5px; - margin: 0; - height: 100%; +.btnsContainer .btnsBlock { + display: flex; } -.sidebar:after { - content: ''; - background-color: #f7f7f7; - position: absolute; - width: 2px; - height: 600px; - top: 10px; - left: 94%; - display: block; +.btnsContainer .btnsBlock button { + margin-left: 1rem; + display: flex; + justify-content: center; + align-items: center; } -.sidebarsticky { - padding-left: 45px; +.btnsContainer .inputContainer { + flex: 1; + position: relative; } -.sidebarsticky > input { - text-decoration: none; - margin-bottom: 50px; - border-color: #e8e5e5; - width: 80%; - border-radius: 7px; - padding-top: 5px; - padding-bottom: 5px; - padding-right: 10px; - padding-left: 10px; - box-shadow: none; +.btnsContainer .input { + width: 70%; + position: relative; } -.navitem { - padding-left: 27%; - padding-top: 12px; - padding-bottom: 12px; - cursor: pointer; +.btnsContainer input { + outline: 1px solid var(--bs-gray-400); } -.searchtitle { - color: #707070; - font-weight: 600; - font-size: 18px; - margin-bottom: 20px; - padding-bottom: 5px; - border-bottom: 3px solid #31bb6b; - width: 60%; +.btnsContainer .inputContainer button { + width: 52px; } -.logintitle { - color: #707070; - font-weight: 600; - font-size: 20px; - margin-bottom: 30px; - padding-bottom: 5px; - border-bottom: 3px solid #31bb6b; - width: 30%; +.largeBtnsWrapper { + display: flex; } -.mainpageright > hr { - margin-top: 20px; +.listBox { width: 100%; - margin-left: -15px; - margin-right: -15px; - margin-bottom: 20px; + flex: 1; } -.justifysp { +.notFound { + flex: 1; display: flex; - justify-content: space-between; -} - -.radio_buttons { - color: #707070; - font-weight: 600; - font-size: 14px; -} -.radio_buttons > input { - transform: scale(1.2); -} -.radio_buttons > label { - margin-top: -4px; - margin-left: 5px; - margin-right: 15px; + justify-content: center; + align-items: center; + flex-direction: column; } -.loader { - text-align: center; -} - -@media screen and (max-width: 575.5px) { - .justifysp { - padding-left: 55px; - display: flex; - justify-content: space-between; - width: 100%; +@media (max-width: 1020px) { + .btnsContainer { + flex-direction: column; + margin: 1.5rem 0; } - .mainpageright { + .btnsContainer .input { width: 100%; } -} - -.list_box { - height: 70vh; - overflow-y: auto; - width: auto; - padding-right: 50px; -} -@media only screen and (max-width: 600px) { - .sidebar { - position: relative; - bottom: 18px; + .btnsContainer .btnsBlock { + margin: 1.5rem 0 0 0; + justify-content: space-between; } - .invitebtn { - width: 135px; - position: relative; - right: 10px; + .btnsContainer .btnsBlock button { + margin: 0; } - .userListTable { - margin-left: 40px; + .btnsContainer .btnsBlock div button { + margin-right: 1.5rem; } } -/* Loader CSS */ - -.loader, -.loader:after { - border-radius: 50%; - width: 10em; - height: 10em; -} +/* For mobile devices */ -.loader { - margin: 60px auto; - margin-top: 35vh !important; - font-size: 10px; - position: relative; - text-indent: -9999em; - border-top: 1.1em solid rgba(255, 255, 255, 0.2); - border-right: 1.1em solid rgba(255, 255, 255, 0.2); - border-bottom: 1.1em solid rgba(255, 255, 255, 0.2); - border-left: 1.1em solid #febc59; - -webkit-transform: translateZ(0); - -ms-transform: translateZ(0); - transform: translateZ(0); - -webkit-animation: load8 1.1s infinite linear; - animation: load8 1.1s infinite linear; -} +@media (max-width: 520px) { + .btnsContainer { + margin-bottom: 0; + } -@-webkit-keyframes load8 { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); + .btnsContainer .btnsBlock { + display: block; + margin-top: 1rem; + margin-right: 0; } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); + .largeBtnsWrapper { + flex-direction: column; } -} -@keyframes load8 { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); + .btnsContainer .btnsBlock div { + flex: 1; } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); + .btnsContainer .btnsBlock button { + margin-bottom: 1rem; + margin-right: 0; + width: 100%; } } diff --git a/src/screens/BlockUser/BlockUser.test.tsx b/src/screens/BlockUser/BlockUser.test.tsx index 7d5b4fbd41..1c8be48624 100644 --- a/src/screens/BlockUser/BlockUser.test.tsx +++ b/src/screens/BlockUser/BlockUser.test.tsx @@ -1,24 +1,24 @@ import React from 'react'; -import { act, render, screen } from '@testing-library/react'; import { MockedProvider } from '@apollo/react-testing'; -import { BrowserRouter } from 'react-router-dom'; -import { Provider } from 'react-redux'; -import { I18nextProvider } from 'react-i18next'; -import BlockUser from './BlockUser'; -import { - BLOCK_PAGE_MEMBER_LIST, - ORGANIZATIONS_LIST, -} from 'GraphQl/Queries/Queries'; +import { act, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { BLOCK_USER_MUTATION, UNBLOCK_USER_MUTATION, } from 'GraphQl/Mutations/mutations'; -import { store } from 'state/store'; -import userEvent from '@testing-library/user-event'; -import i18nForTest from 'utils/i18nForTest'; -import { StaticMockLink } from 'utils/StaticMockLink'; +import { + BLOCK_PAGE_MEMBER_LIST, + ORGANIZATIONS_LIST, +} from 'GraphQl/Queries/Queries'; import 'jest-location-mock'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; import { ToastContainer } from 'react-toastify'; +import { store } from 'state/store'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import i18nForTest from 'utils/i18nForTest'; +import BlockUser from './BlockUser'; let userQueryCalled = false; @@ -226,15 +226,87 @@ const MOCKS = [ request: { query: BLOCK_PAGE_MEMBER_LIST, variables: { - firstName_contains: 'sam', - lastName_contains: 'smith', + firstName_contains: 'Peter', + lastName_contains: '', + orgId: 'orgid', + }, + }, + result: { + data: { + organizationsMemberConnection: { + edges: [], + }, + }, + }, + }, +]; +const MOCKS_EMPTY = [ + { + request: { + query: ORGANIZATIONS_LIST, + variables: { + id: 'orgid', + }, + }, + result: { + data: { + organizations: [ + { + _id: 'orgid', + image: '', + creator: { + firstName: 'firstName', + lastName: 'lastName', + email: 'email', + }, + name: 'name', + description: 'description', + location: 'location', + members: { + _id: 'id', + firstName: 'firstName', + lastName: 'lastName', + email: 'email', + }, + admins: { + _id: 'id', + firstName: 'firstName', + lastName: 'lastName', + email: 'email', + }, + membershipRequests: { + _id: 'id', + user: { + firstName: 'firstName', + lastName: 'lastName', + email: 'email', + }, + }, + blockedUsers: { + _id: 'id', + firstName: 'firstName', + lastName: 'lastName', + email: 'email', + }, + }, + ], + }, + }, + }, + + { + request: { + query: BLOCK_PAGE_MEMBER_LIST, + variables: { + firstName_contains: 'Peter', + lastName_contains: '', orgId: 'orgid', }, }, result: { data: { organizationsMemberConnection: { - edges: [USER_UNBLOCKED], + edges: [], }, }, }, @@ -242,6 +314,7 @@ const MOCKS = [ ]; const link = new StaticMockLink(MOCKS, true); +const link2 = new StaticMockLink(MOCKS_EMPTY, true); async function wait(ms = 500): Promise { await act(() => { @@ -273,7 +346,7 @@ describe('Testing Block/Unblock user screen', () => { await wait(); - expect(screen.getByText('Search By Name')).toBeInTheDocument(); + expect(screen.getByText('Search By First Name')).toBeInTheDocument(); expect(screen.getByText('List of Users who spammed')).toBeInTheDocument(); expect(window.location).toBeAt('/blockuser/id=orgid'); @@ -357,14 +430,15 @@ describe('Testing Block/Unblock user screen', () => { expect(screen.getByText('John Doe')).toBeInTheDocument(); expect(screen.getByText('Sam Smith')).toBeInTheDocument(); - const firstNameInput = screen.getByPlaceholderText(/Enter First Name/i); - const lastNameInput = screen.getByPlaceholderText(/Enter Last Name/i); - + const firstNameInput = screen.getByPlaceholderText(/Search by First Name/i); + // Open Dropdown + await act(async () => { + userEvent.click(screen.getByTestId('nameFilter')); + }); + // Select option and enter first name + userEvent.click(screen.getByTestId('searchByFirstName')); userEvent.type(firstNameInput, 'john'); - expect(firstNameInput).toHaveValue('john'); - expect(lastNameInput).toHaveValue(''); - await wait(700); expect(screen.getByText('John Doe')).toBeInTheDocument(); @@ -393,32 +467,31 @@ describe('Testing Block/Unblock user screen', () => { expect(screen.getByText('John Doe')).toBeInTheDocument(); expect(screen.getByText('Sam Smith')).toBeInTheDocument(); - const firstNameInput = screen.getByPlaceholderText(/Enter First Name/i); - const lastNameInput = screen.getByPlaceholderText(/Enter Last Name/i); - + // Open Dropdown + await act(async () => { + userEvent.click(screen.getByTestId('nameFilter')); + }); + // Select option and enter last name + userEvent.click(screen.getByTestId('searchByLastName')); + const lastNameInput = screen.getByPlaceholderText(/Search by Last Name/i); userEvent.type(lastNameInput, 'doe'); await wait(700); - expect(firstNameInput).toHaveValue(''); expect(lastNameInput).toHaveValue('doe'); - expect(screen.getByText('John Doe')).toBeInTheDocument(); expect(screen.queryByText('Sam Smith')).not.toBeInTheDocument(); - expect(window.location).toBeAt('/blockuser/id=orgid'); }); - test('Testing Full Name Filter', async () => { + test('Testing No Spammers Present', async () => { window.location.assign('/blockuser/id=orgid'); - render( - + - @@ -426,24 +499,7 @@ describe('Testing Block/Unblock user screen', () => { ); await wait(); - - expect(screen.getByText('John Doe')).toBeInTheDocument(); - expect(screen.getByText('Sam Smith')).toBeInTheDocument(); - - const firstNameInput = screen.getByPlaceholderText(/Enter First Name/i); - const lastNameInput = screen.getByPlaceholderText(/Enter Last Name/i); - - userEvent.type(firstNameInput, 'sam'); - userEvent.type(lastNameInput, 'smith'); - - expect(firstNameInput).toHaveValue('sam'); - expect(lastNameInput).toHaveValue('smith'); - - await wait(700); - - expect(screen.getByText('Sam Smith')).toBeInTheDocument(); - expect(screen.queryByText('John Doe')).not.toBeInTheDocument(); - + expect(screen.getByText(/No spammer found/i)).toBeInTheDocument(); expect(window.location).toBeAt('/blockuser/id=orgid'); }); @@ -462,15 +518,15 @@ describe('Testing Block/Unblock user screen', () => { ); - - await wait(); - - userEvent.click(screen.getByLabelText(/All Members/i)); await wait(); + await act(async () => { + userEvent.click(screen.getByTestId('userFilter')); + }); + userEvent.click(screen.getByTestId('showMembers')); - expect(screen.getByLabelText(/All Members/i)).toBeChecked(); - await wait(); + await wait(700); + expect(screen.getByTestId(/userFilter/i)).toHaveTextContent('All Members'); expect(screen.getByText('John Doe')).toBeInTheDocument(); expect(screen.getByText('Sam Smith')).toBeInTheDocument(); @@ -493,12 +549,11 @@ describe('Testing Block/Unblock user screen', () => { ); - await wait(); - - userEvent.click(screen.getByLabelText(/Blocked Users/i)); - await wait(); + await act(async () => { + userEvent.click(screen.getByTestId('userFilter')); + }); - expect(screen.getByLabelText(/Blocked Users/i)).toBeChecked(); + userEvent.click(screen.getByTestId('showBlockedMembers')); await wait(); expect(screen.getByText('John Doe')).toBeInTheDocument(); @@ -524,9 +579,34 @@ describe('Testing Block/Unblock user screen', () => { await wait(); - expect(screen.getByTestId(/blockedusers/)).toBeInTheDocument(); - expect(screen.getByTestId(/allusers/)).toBeInTheDocument(); + expect(screen.getByTestId(/userList/)).toBeInTheDocument(); + expect(screen.getAllByText('Block/Unblock')).toHaveLength(2); expect(screen.getByText('John Doe')).toBeInTheDocument(); expect(screen.getByText('Sam Smith')).toBeInTheDocument(); }); + + test('Testing No Results Found', async () => { + window.location.assign('/blockuser/id=orgid'); + render( + + + + + + + + + + ); + + const input = screen.getByPlaceholderText('Search By First Name'); + await act(async () => { + userEvent.type(input, 'Peter'); + }); + await wait(700); + expect( + screen.getByText(`No results found for "Peter"`) + ).toBeInTheDocument(); + expect(window.location).toBeAt('/blockuser/id=orgid'); + }); }); diff --git a/src/screens/BlockUser/BlockUser.tsx b/src/screens/BlockUser/BlockUser.tsx index 1995c2d634..2b39a00650 100644 --- a/src/screens/BlockUser/BlockUser.tsx +++ b/src/screens/BlockUser/BlockUser.tsx @@ -1,17 +1,18 @@ import { useMutation, useQuery } from '@apollo/client'; -import React, { useEffect, useRef, useState } from 'react'; -import { Col, Form, Row } from 'react-bootstrap'; +import React, { useEffect, useState } from 'react'; +import { Dropdown, Form, Table } from 'react-bootstrap'; import Button from 'react-bootstrap/Button'; import { toast } from 'react-toastify'; -import { CircularProgress } from '@mui/material'; +import { Search } from '@mui/icons-material'; +import SortIcon from '@mui/icons-material/Sort'; import { BLOCK_USER_MUTATION, UNBLOCK_USER_MUTATION, } from 'GraphQl/Mutations/mutations'; import { BLOCK_PAGE_MEMBER_LIST } from 'GraphQl/Queries/Queries'; import OrganizationScreen from 'components/OrganizationScreen/OrganizationScreen'; -import PaginationList from 'components/PaginationList/PaginationList'; +import TableLoader from 'components/TableLoader/TableLoader'; import { useTranslation } from 'react-i18next'; import debounce from 'utils/debounce'; import { errorHandler } from 'utils/errorHandler'; @@ -35,21 +36,15 @@ const Requests = (): JSX.Element => { }); document.title = t('title'); - - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = React.useState(10); - const currentUrl = window.location.href.split('=')[1]; - const [membersData, setMembersData] = useState([]); - const [state, setState] = useState(0); - - const firstNameRef = useRef(null); - const lastNameRef = useRef(null); + const [searchByFirstName, setSearchByFirstName] = useState(true); + const [searchByName, setSearchByName] = useState(''); + const [showBlockedMembers, setShowBlockedMembers] = useState(false); const { data: memberData, - loading: memberLoading, + loading: loadingMembers, error: memberError, refetch: memberRefetch, } = useQuery(BLOCK_PAGE_MEMBER_LIST, { @@ -69,33 +64,16 @@ const Requests = (): JSX.Element => { return; } - if (state === 0) { + if (showBlockedMembers == false) { setMembersData(memberData?.organizationsMemberConnection.edges); } else { const blockUsers = memberData?.organizationsMemberConnection.edges.filter( (user: InterfaceMember) => user.organizationsBlockedBy.some((org) => org._id === currentUrl) ); - setMembersData(blockUsers); } - }, [state, memberData]); - - /* 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); - }; + }, [memberData, showBlockedMembers]); const handleBlockUser = async (userId: string): Promise => { try { @@ -140,181 +118,175 @@ const Requests = (): JSX.Element => { toast.error(memberError.message); } - const handleSearch = (): void => { - const filterData = { + const handleSearch = (e: any): void => { + const { value } = e.target; + setSearchByName(value); + memberRefetch({ orgId: currentUrl, - firstName_contains: firstNameRef.current?.value ?? '', - lastName_contains: lastNameRef.current?.value ?? '', - }; - - memberRefetch(filterData); + firstName_contains: searchByFirstName ? value : '', + lastName_contains: searchByFirstName ? '' : value, + }); }; const handleSearchDebounced = debounce(handleSearch); + const headerTitles: string[] = [ + '#', + t('name'), + t('email'), + t('block_unblock'), + ]; return ( <> - - - -
-
-
{t('searchByName')}
- - - - -
- { - setState(0); - }} - /> - - - { - setState(1); - }} - /> - -
-
+ + {/* Buttons Container */} +
+
+
+ +
- - - -
- -

{t('listOfUsers')}

-
- {memberLoading ? ( -
- -
- ) : ( -
-
- - - - - - - - - - - - { - /* istanbul ignore next */ - (rowsPerPage > 0 - ? membersData.slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage - ) - : membersData - ).map((user, index: number) => { - return ( - - - - - - - ); - }) - } - -
#{t('name')}{t('email')} - {t('block_unblock')} -
{page * 10 + (index + 1)}{`${user.firstName} ${user.lastName}`}{user.email} - {user.organizationsBlockedBy.some( - (spam: any) => spam._id === currentUrl - ) ? ( - - ) : ( - - )} -
-
-
- )} -
- - - - - - -
-
+
+
+
+ +
- - +
+
+ {/* Table */} + {loadingMembers == false && + membersData.length === 0 && + searchByName.length > 0 ? ( +
+

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

+
+ ) : loadingMembers == false && membersData.length === 0 ? ( +
+

{t('noSpammerFound')}

+
+ ) : ( +
+ {loadingMembers ? ( + + ) : ( + + + + {headerTitles.map((title: string, index: number) => { + return ( + + ); + })} + + + + {membersData.map((user, index: number) => { + return ( + + + + + + + ); + })} + +
+ {title} +
{index + 1}{`${user.firstName} ${user.lastName}`}{user.email} + {user.organizationsBlockedBy.some( + (spam: any) => spam._id === currentUrl + ) ? ( + + ) : ( + + )} +
+ )} +
+ )} ); diff --git a/src/screens/ForgotPassword/ForgotPassword.module.css b/src/screens/ForgotPassword/ForgotPassword.module.css index bd59589a61..e69de29bb2 100644 --- a/src/screens/ForgotPassword/ForgotPassword.module.css +++ b/src/screens/ForgotPassword/ForgotPassword.module.css @@ -1,17 +0,0 @@ -.forgotPassword .border { - border-color: #31bb6b !important; -} -.forgotPassword .heading h1 { - color: #31bb6b; -} - -.talawaBackgroundColor { - background-color: #31bb6b; -} - -@media only screen and (max-width: 600px) { - .forgotPassword .border { - position: relative; - bottom: 50px; - } -} diff --git a/src/screens/ForgotPassword/ForgotPassword.tsx b/src/screens/ForgotPassword/ForgotPassword.tsx index eb75422385..eb96f70f26 100644 --- a/src/screens/ForgotPassword/ForgotPassword.tsx +++ b/src/screens/ForgotPassword/ForgotPassword.tsx @@ -14,6 +14,7 @@ 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'; const ForgotPassword = (): JSX.Element => { const { t } = useTranslation('translation', { @@ -113,7 +114,7 @@ const ForgotPassword = (): JSX.Element => { }; if (componentLoader || otpLoading || forgotPasswordLoading) { - return
; + return ; } return ( diff --git a/src/screens/OrgSettings/OrgSettings.module.css b/src/screens/OrgSettings/OrgSettings.module.css index 88e7ab719c..2b15a2ac0c 100644 --- a/src/screens/OrgSettings/OrgSettings.module.css +++ b/src/screens/OrgSettings/OrgSettings.module.css @@ -1,184 +1,25 @@ -.navbarbg { - height: 60px; - background-color: white; - display: flex; - margin-bottom: 30px; - z-index: 1; - position: relative; - flex-direction: row; - justify-content: space-between; - box-shadow: 0px 0px 8px 2px #c8c8c8; -} -.titlemodal .logo { - color: #707070; - margin-left: 0; - display: flex; - align-items: center; - text-decoration: none; -} - -.logo img { - margin-top: 0px; - margin-left: 10px; - height: 64px; - width: 70px; -} - -.logo > strong { - line-height: 1.5rem; - margin-left: -5px; - font-family: sans-serif; - font-size: 19px; - color: #707070; -} - -.mainpage { - display: flex; - flex-direction: row; -} -.sidebar { - z-index: 0; - padding-top: 5px; - margin: 0; - height: 100%; -} -.sidebar:after { - content: ''; - background-color: #f7f7f7; - position: absolute; - width: 2px; - height: 100%; - top: 10px; - left: 94%; - display: block; -} -.sidebarsticky { - padding-left: 45px; - margin-top: 7px; -} -.sidebarsticky > p { - margin-top: -10px; +.settingsBody { + margin: 2.5rem 0; } -.navitem { - padding-left: 27%; - padding-top: 12px; - padding-bottom: 12px; - cursor: pointer; -} - -.headerDiv { +.cardHeader { + padding: 1.25rem 1rem 1rem 1rem; + border-bottom: 1px solid var(--bs-gray-200); display: flex; - flex-direction: column; + justify-content: space-between; + align-items: center; } -.logintitle { - color: #707070; - font-weight: 600; - font-size: 20px; - /* margin-bottom: 30px; */ - padding-bottom: 5px; - border-bottom: 3px solid #31bb6b; - /* width: 15%; */ - margin-left: 20px; -} -.loginSubtitle { - color: #707070; - font-weight: 600; - font-size: 19px; - /* margin-bottom: 30px; */ - padding-bottom: 5px; - /* border-bottom: 3px solid #31bb6b; */ - /* width: 15%; */ - margin-left: 20px; -} -.searchtitle { - color: #707070; +.cardHeader .cardTitle { + font-size: 1.2rem; font-weight: 600; - font-size: 18px; - margin-bottom: 20px; - padding-bottom: 5px; - border-bottom: 3px solid #31bb6b; - width: 60%; -} -.logintitleadmin { - color: #707070; - font-weight: 600; - font-size: 18px; - margin-top: 50px; - margin-bottom: 40px; - padding-bottom: 5px; - border-bottom: 3px solid #31bb6b; - width: 30%; -} -.greenregbtn { - margin: 1rem 0 0; - margin-top: 15px; - border: 1px solid #e8e5e5; - box-shadow: 0 2px 2px #e8e5e5; - padding: 6px 8px; - border-radius: 5px; - background-color: #31bb6b; - width: 70%; - font-size: 14px; - color: white; - outline: none; - font-weight: 600; - cursor: pointer; - transition: transform 0.2s, box-shadow 0.2s; -} -.sidebarsticky > input { - text-decoration: none; - margin-bottom: 50px; - border-color: #e8e5e5; - width: 80%; - border-radius: 7px; - padding-top: 5px; - padding-bottom: 5px; - padding-right: 10px; - padding-left: 10px; - box-shadow: none; } -.loader, -.loader:after { - border-radius: 50%; - width: 10em; - height: 10em; -} -.loader { - margin: 60px auto; - margin-top: 35vh !important; - font-size: 10px; - position: relative; - text-indent: -9999em; - border-top: 1.1em solid rgba(255, 255, 255, 0.2); - border-right: 1.1em solid rgba(255, 255, 255, 0.2); - border-bottom: 1.1em solid rgba(255, 255, 255, 0.2); - border-left: 1.1em solid #febc59; - -webkit-transform: translateZ(0); - -ms-transform: translateZ(0); - transform: translateZ(0); - -webkit-animation: load8 1.1s infinite linear; - animation: load8 1.1s infinite linear; +.cardBody { + min-height: 180px; } -@-webkit-keyframes load8 { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } -} -@keyframes load8 { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } + +.cardBody .textBox { + margin: 0 0 3rem 0; + color: var(--bs-secondary); } diff --git a/src/screens/OrgSettings/OrgSettings.test.tsx b/src/screens/OrgSettings/OrgSettings.test.tsx index 2fe1b816ef..e722dc4a8d 100644 --- a/src/screens/OrgSettings/OrgSettings.test.tsx +++ b/src/screens/OrgSettings/OrgSettings.test.tsx @@ -1,37 +1,76 @@ import React from 'react'; import { MockedProvider } from '@apollo/react-testing'; -import { act, render, screen } from '@testing-library/react'; -import { MEMBERSHIP_REQUEST } from 'GraphQl/Queries/Queries'; +import { render, screen } from '@testing-library/react'; +import 'jest-location-mock'; +import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; -import userEvent from '@testing-library/user-event'; -import { I18nextProvider } from 'react-i18next'; -import 'jest-location-mock'; +import { DELETE_ORGANIZATION_MUTATION } from 'GraphQl/Mutations/mutations'; import { store } from 'state/store'; -import OrgSettings from './OrgSettings'; -import i18nForTest from 'utils/i18nForTest'; import { StaticMockLink } from 'utils/StaticMockLink'; +import i18nForTest from 'utils/i18nForTest'; +import OrgSettings from './OrgSettings'; +import { ORGANIZATIONS_LIST } from 'GraphQl/Queries/Queries'; const MOCKS = [ { request: { - query: MEMBERSHIP_REQUEST, + query: ORGANIZATIONS_LIST, }, result: { data: { organizations: [ { - _id: 1, - membershipRequests: { - _id: 1, - user: { - _id: 1, + _id: '123', + image: null, + name: 'Palisadoes', + description: 'Equitable Access to STEM Education Jobs', + location: 'Jamaica', + isPublic: true, + visibleInSearch: false, + creator: { + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@example.com', + }, + members: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', + }, + admins: [ + { + _id: '123', firstName: 'John', lastName: 'Doe', email: 'johndoe@gmail.com', }, + ], + membershipRequests: { + _id: '456', + user: { + firstName: 'Sam', + lastName: 'Smith', + email: 'samsmith@gmail.com', + }, }, + blockedUsers: [], + }, + ], + }, + }, + }, + { + request: { + query: DELETE_ORGANIZATION_MUTATION, + }, + result: { + data: { + removeOrganization: [ + { + _id: 123, }, ], }, @@ -41,36 +80,24 @@ const MOCKS = [ const link = new StaticMockLink(MOCKS, true); -async function wait(ms = 100): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); -} +afterEach(() => { + localStorage.clear(); +}); describe('Organisation Settings Page', () => { test('correct mock data should be queried', async () => { - const dataQuery1 = MOCKS[0]?.result?.data?.organizations[0]; - - expect(dataQuery1).toEqual({ - _id: 1, - membershipRequests: { - _id: 1, - user: { - _id: 1, - email: 'johndoe@gmail.com', - firstName: 'John', - lastName: 'Doe', - }, + const dataQuery1 = MOCKS[1]?.result?.data?.removeOrganization; + expect(dataQuery1).toEqual([ + { + _id: 123, }, - }); + ]); }); test('should render props and text elements test for the screen', async () => { - window.location.assign('/orglist'); - - const { container } = render( + window.location.assign('/orgsetting/id=123'); + localStorage.setItem('UserType', 'SUPERADMIN'); + render( @@ -81,131 +108,14 @@ describe('Organisation Settings Page', () => { ); - - await wait(); - expect(container.textContent).not.toBe('Loading data...'); - - expect(container.textContent).toMatch('Settings'); - expect(container.textContent).toMatch('Update Your Details'); - expect(container.textContent).toMatch('Update Organization'); - expect(container.textContent).toMatch('Delete Organization'); - expect(container.textContent).toMatch('See Request'); - - expect(window.location).toBeAt('/orglist'); - }); - - test('should render User update form in clicking user update button', async () => { - window.location.assign('/orglist'); - - const { container } = render( - - - - - - - - - - ); - - await wait(); - expect(container.textContent).not.toBe('Loading data...'); - await wait(); - - userEvent.click(screen.getByTestId('userUpdateBtn')); - - await wait(); - const firstNameInput = screen.getByText(/first name/i); - const lastNameInput = screen.getByText(/last name/i); - const emailInput = screen.getByText(/email/i); - const imageInput = screen.getByText(/display image:/i); - const saveBtn = screen.getByRole('button', { name: /save changes/i }); - const cancelBtn = screen.getByRole('button', { name: /cancel/i }); - - await wait(); - - expect(firstNameInput).toBeInTheDocument(); - expect(lastNameInput).toBeInTheDocument(); - expect(emailInput).toBeInTheDocument(); - expect(imageInput).toBeInTheDocument(); - expect(saveBtn).toBeInTheDocument(); - expect(cancelBtn).toBeInTheDocument(); - }); - - test('should render password update form in clicking update your password button', async () => { - window.location.assign('/orglist'); - - const { container } = render( - - - - - - - - - - ); - - expect(container.textContent).not.toBe('Loading data...'); - await wait(); - - userEvent.click(screen.getByTestId('userPasswordUpdateBtn')); - - await wait(); - const previousPasswordInput = screen.getByText(/previous password/i); - const confirmPasswordInput = screen.getByText(/confirm new password/i); - const saveBtn = screen.getByRole('button', { name: /save changes/i }); - const cancelBtn = screen.getByRole('button', { name: /cancel/i }); - - await wait(); - - expect(previousPasswordInput).toBeInTheDocument(); - expect(confirmPasswordInput).toBeInTheDocument(); - expect(saveBtn).toBeInTheDocument(); - expect(cancelBtn).toBeInTheDocument(); - }); - - test('should render update orgnization form in clicking update orgnization button', async () => { - window.location.assign('/orglist'); - - const { container } = render( - - - - - - - - - - ); - - expect(container.textContent).not.toBe('Loading data...'); - await wait(); - - userEvent.click(screen.getByTestId('orgUpdateBtn')); - - await wait(); - const nameInput = screen.getByText(/name/i); - const descriptionInput = screen.getByText(/description/i); - const locationInput = screen.getByText(/location/i); - const displayImageInput = screen.getByText(/display image:/i); - const isPublicInput = screen.getByText(/is public:/i); - const isRegistrableInput = screen.getByText(/is registrable:/i); - const saveBtn = screen.getByRole('button', { name: /save changes/i }); - const cancelBtn = screen.getByRole('button', { name: /cancel/i }); - - await wait(); - - expect(nameInput).toBeInTheDocument(); - expect(descriptionInput).toBeInTheDocument(); - expect(locationInput).toBeInTheDocument(); - expect(displayImageInput).toBeInTheDocument(); - expect(isPublicInput).toBeInTheDocument(); - expect(isRegistrableInput).toBeInTheDocument(); - expect(saveBtn).toBeInTheDocument(); - expect(cancelBtn).toBeInTheDocument(); + 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 + ) + ).toBeInTheDocument(); + expect(screen.getByText(/Other Settings/i)).toBeInTheDocument(); + expect(screen.getByText(/Change Language/i)).toBeInTheDocument(); + expect(window.location).toBeAt('/orgsetting/id=123'); }); }); diff --git a/src/screens/OrgSettings/OrgSettings.tsx b/src/screens/OrgSettings/OrgSettings.tsx index ef707bf91f..d0df92b2c6 100644 --- a/src/screens/OrgSettings/OrgSettings.tsx +++ b/src/screens/OrgSettings/OrgSettings.tsx @@ -1,15 +1,9 @@ -import { useQuery } from '@apollo/client'; -import { MEMBERSHIP_REQUEST } from 'GraphQl/Queries/Queries'; -import defaultImg from 'assets/images/blank.png'; -import Loader from 'components/Loader/Loader'; -import MemberRequestCard from 'components/MemberRequestCard/MemberRequestCard'; -import OrgDelete from 'components/OrgDelete/OrgDelete'; +import React from 'react'; +import ChangeLanguageDropDown from 'components/ChangeLanguageDropdown/ChangeLanguageDropDown'; +import DeleteOrg from 'components/DeleteOrg/DeleteOrg'; import OrgUpdate from 'components/OrgUpdate/OrgUpdate'; import OrganizationScreen from 'components/OrganizationScreen/OrganizationScreen'; -import UserPasswordUpdate from 'components/UserPasswordUpdate/UserPasswordUpdate'; -import UserUpdate from 'components/UserUpdate/UserUpdate'; -import React from 'react'; -import Button from 'react-bootstrap/Button'; +import { Card, Form } from 'react-bootstrap'; import Col from 'react-bootstrap/Col'; import Row from 'react-bootstrap/Row'; import { useTranslation } from 'react-i18next'; @@ -21,171 +15,39 @@ function orgSettings(): JSX.Element { }); document.title = t('title'); - const [screenVariable, setScreenVariable] = React.useState(0); - const [screenDisplayVariable, setDisplayScreenVariable] = React.useState(''); - - const handleClick = (number: any): void => { - if (number === 1) { - setDisplayScreenVariable('updateYourDetails'); - setScreenVariable(1); - } else if (number === 2) { - setDisplayScreenVariable('updateOrganization'); - setScreenVariable(2); - } else if (number === 3) { - setDisplayScreenVariable('deleteOrganization'); - setScreenVariable(3); - } else if (number === 4) { - setDisplayScreenVariable('seeRequest'); - setScreenVariable(4); - } else { - setDisplayScreenVariable('updateYourPassword'); - setScreenVariable(5); - } - }; - const currentUrl = window.location.href.split('=')[1]; - const { data, loading, error } = useQuery(MEMBERSHIP_REQUEST, { - variables: { id: currentUrl }, - }); - - if (loading) { - return ; - } - - /* istanbul ignore next */ - if (error) { - window.location.replace('/orglist'); - } - return ( <> - - - -
-
-
- - - - - + + + + +
+
+ {t('updateOrganization')}
-
+ + {currentUrl && } + + - -
- -
-

{t('settings')}

- {screenDisplayVariable != '' && ( -

- {t(screenDisplayVariable)} -

- )} - {/*

{t("abc")}

*/} -
- - {/*

{t('settings')}

*/} -
-
{screenVariable == 1 ? : null}
-
- {screenVariable == 5 ? : null} + + + +
+
{t('otherSettings')}
-
- {screenVariable == 2 ? ( - - ) : null} -
-
{screenVariable == 3 ? : null}
-
- {screenVariable == 4 ? ( - data?.organizations?.membershipRequests ? ( - /* istanbul ignore next */ - data.organizations.map( - /* istanbul ignore next */ - (datas: { - _id: string; - membershipRequests: { - _id: string; - user: { - _id: string; - firstName: string; - lastName: string; - email: string; - }; - }; - }) => { - /* istanbul ignore next */ - return ( - - ); - } - ) - ) : ( -
{t('noData')}
- ) - ) : null} -
-
+ +
+ + {t('changeLanguage')} + + +
+
+ diff --git a/src/screens/OrganizationDashboard/OrganizationDashboard.module.css b/src/screens/OrganizationDashboard/OrganizationDashboard.module.css index af35265ae8..485200b1ae 100644 --- a/src/screens/OrganizationDashboard/OrganizationDashboard.module.css +++ b/src/screens/OrganizationDashboard/OrganizationDashboard.module.css @@ -1,197 +1,24 @@ -.mainpage { +.cardHeader { + padding: 1.25rem 1rem 1rem 1rem; + border-bottom: 1px solid var(--bs-gray-200); display: flex; - flex-direction: row; -} - -.toporgloc { - padding-top: 8px; - font-size: 16px; -} -.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; -} -.sidebarsticky { - padding-left: 30px; -} -.sidebarsticky > p { - margin-top: -10px; - width: 90%; -} - -.description { - word-wrap: break-word; -} - -.titlename { - color: #707070; - font-weight: 600; - font-size: 20px; - margin-bottom: 30px; - padding-bottom: 5px; - width: 26%; -} -.tagdetailsGreen > button { - background-color: #31bb6b; - color: white; - outline: none; - cursor: pointer; - transition: transform 0.2s, box-shadow 0.2s; - border: none; - border-radius: 5px; - margin-top: -12px; - margin-bottom: 10px; - margin-right: 30px; - padding-right: 20px; - padding-left: 20px; - padding-top: 5px; - padding-bottom: 5px; -} -.mainpageright > hr { - margin-top: 20px; - width: 100%; - margin-left: -15px; - margin-right: -15px; - margin-bottom: 20px; -} -.justifysp { - display: flex; - justify-content: space-between; -} -.org_about_img { - margin-top: 0px; - margin-bottom: 30px; - border-radius: 5px; - max-width: 100%; - height: auto; - width: 90%; -} -.invitebtn { - border: 1px solid #e8e5e5; - box-shadow: 0 2px 2px #e8e5e5; - border-radius: 5px; - background-color: #31bb6b; - width: 20%; - height: 40px; - font-size: 16px; - color: white; - outline: none; - font-weight: 600; - cursor: pointer; - transition: transform 0.2s, box-shadow 0.2s; -} -.flexdir { - display: flex; - flex-direction: row; justify-content: space-between; - border: none; + align-items: center; } -.logintitleinvite { - color: #707070; +.cardHeader .cardTitle { + font-size: 1.2rem; font-weight: 600; - font-size: 20px; - margin-bottom: 20px; - padding-bottom: 5px; - border-bottom: 3px solid #31bb6b; - width: 40%; -} - -.cancel > i { - margin-top: 5px; - transform: scale(1.2); - cursor: pointer; - color: #707070; -} - -.greenregbtn { - margin: 1rem 0 0; - margin-top: 10px; - border: 1px solid #e8e5e5; - box-shadow: 0 2px 2px #e8e5e5; - padding: 10px 10px; - border-radius: 5px; - background-color: #31bb6b; - width: 100%; - font-size: 16px; - color: white; - outline: none; - font-weight: 600; - cursor: pointer; - transition: transform 0.2s, box-shadow 0.2s; - width: 100%; -} - -.loader, -.loader:after { - border-radius: 50%; - width: 10em; - height: 10em; -} -.loader { - margin: 60px auto; - margin-top: 35vh !important; - font-size: 10px; - position: relative; - text-indent: -9999em; - border-top: 1.1em solid rgba(255, 255, 255, 0.2); - border-right: 1.1em solid rgba(255, 255, 255, 0.2); - border-bottom: 1.1em solid rgba(255, 255, 255, 0.2); - border-left: 1.1em solid #febc59; - -webkit-transform: translateZ(0); - -ms-transform: translateZ(0); - transform: translateZ(0); - -webkit-animation: load8 1.1s infinite linear; - animation: load8 1.1s infinite linear; -} -@-webkit-keyframes load8 { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } -} -@keyframes load8 { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } } -.cardContainer { - box-shadow: 0 5px 20px rgba(0, 0, 0, 0.05); +.cardBody { + min-height: 180px; + padding-top: 0; } -.dashboardIcon { - font-size: 50px; - color: #31bb6b; -} - -.counterNumber { - font-size: 24px; - margin-bottom: 0rem !important; -} - -.counterHead { - color: #99abb4; - margin-bottom: 0rem !important; +.cardBody .emptyContainer { + display: flex; + height: 180px; + justify-content: center; + align-items: center; } diff --git a/src/screens/OrganizationDashboard/OrganizationDashboard.test.tsx b/src/screens/OrganizationDashboard/OrganizationDashboard.test.tsx index 612bbd30de..61d3eebc60 100644 --- a/src/screens/OrganizationDashboard/OrganizationDashboard.test.tsx +++ b/src/screens/OrganizationDashboard/OrganizationDashboard.test.tsx @@ -1,27 +1,19 @@ import React from 'react'; import { MockedProvider } from '@apollo/react-testing'; -import type { RenderResult } from '@testing-library/react'; -import { - act, - render, - screen, - fireEvent, - waitFor, -} from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import 'jest-location-mock'; import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; -import OrganizationDashboard from './OrganizationDashboard'; -import { - MOCKS_WITHOUT_IMAGE, - MOCKS_WITH_IMAGE, -} from './OrganizationDashboardMocks'; import { store } from 'state/store'; -import i18nForTest from 'utils/i18nForTest'; -import { USER_ORGANIZATION_LIST } from 'GraphQl/Queries/Queries'; import { StaticMockLink } from 'utils/StaticMockLink'; +import OrganizationDashboard from './OrganizationDashboard'; +import { EMPTY_MOCKS, ERROR_MOCKS, MOCKS } from './OrganizationDashboardMocks'; +import i18nForTest from 'utils/i18nForTest'; +import dayjs from 'dayjs'; +import { toast } from 'react-toastify'; +import userEvent from '@testing-library/user-event'; async function wait(ms = 100): Promise { await act(() => { @@ -30,125 +22,124 @@ async function wait(ms = 100): Promise { }); }); } -const link2 = new StaticMockLink(MOCKS_WITH_IMAGE, true); -const link3 = new StaticMockLink(MOCKS_WITHOUT_IMAGE, true); -const customRender = (userType: any): RenderResult => { - const mockedUser = { - request: { - query: USER_ORGANIZATION_LIST, - variables: { id: localStorage.getItem('id') }, - }, - result: { - data: { - user: { - userType, - firstName: 'John', - lastName: 'Doe', - image: '', - email: 'John_Does_Palasidoes@gmail.com', - adminFor: { - _id: 1, - name: 'Akatsuki', - image: '', - }, - }, - }, - }, - }; - - const mocks = [mockedUser, ...MOCKS_WITHOUT_IMAGE]; - - const link1 = new StaticMockLink(mocks, true); - - return render( - - - - - - - - - +const link1 = new StaticMockLink(MOCKS, true); +const link2 = new StaticMockLink(EMPTY_MOCKS, true); +const link3 = new StaticMockLink(ERROR_MOCKS, true); + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); + +beforeEach(() => { + localStorage.setItem('FirstName', 'John'); + localStorage.setItem('LastName', 'Doe'); + localStorage.setItem('UserType', 'SUPERADMIN'); + localStorage.setItem( + 'UserImage', + 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe' ); -}; +}); + +afterEach(() => { + jest.clearAllMocks(); + localStorage.clear(); +}); describe('Organisation Dashboard Page', () => { - test('should render props and text elements test for the screen', async () => { - window.location.replace('/orglist'); - - const { container } = render( - - - - - - - - - - ); - - expect(container.textContent).not.toBe('Loading data...'); + test('Should render props and text elements test for the screen', async () => { + await act(async () => { + render( + + + + + + + + + + ); + }); + await wait(); - expect(container.textContent).toMatch('Location'); - expect(container.textContent).toMatch('About'); - expect(container.textContent).toMatch('Statistics'); - expect(window.location).toBeAt('/orglist'); - }); + expect(screen.getByText('Members')).toBeInTheDocument(); + expect(screen.getByText('Admins')).toBeInTheDocument(); + expect(screen.getAllByText('Posts')).toHaveLength(2); + 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('Membership requests')).toBeInTheDocument(); + expect(screen.getAllByText('View all')).toHaveLength(3); - test('should display delete button for SUPERADMIN', async () => { - const { getByTestId, queryByTestId } = customRender('SUPERADMIN'); - await waitFor(() => - expect(queryByTestId('deleteClick')).toBeInTheDocument() - ); + // Checking if events are rendered + expect(screen.getByText('Event 1')).toBeInTheDocument(); + expect( + screen.getByText( + `${dayjs(new Date()).add(1, 'day').format('DD-MM-YYYY')}` + ) + ).toBeInTheDocument(); - fireEvent.click(getByTestId('deleteClick')); - fireEvent.click(getByTestId(/deleteOrganizationBtn/i)); - expect(window.location).not.toBeNull(); - }); + // Checking if posts are rendered + expect(screen.getByText('Post 1')).toBeInTheDocument(); - test('should not display delete button for non-SUPERADMIN', async () => { - const { queryByTestId } = customRender('ADMIN'); - await waitFor(() => - expect(queryByTestId('deleteClick')).not.toBeInTheDocument() - ); + // Checking if membership requests are rendered + expect(screen.getByText('Jane Doe')).toBeInTheDocument(); }); - test('Should check if organisation image is present', async () => { - const { container } = render( - - - - - - - - - - ); - - expect(container.textContent).not.toBe('Loading data...'); + test('Testing buttons and checking empty events, posts and membership requests', async () => { + await act(async () => { + render( + + + + + + + + + + ); + }); + await wait(); - const image = screen.getByTestId(/orgDashImgPresent/i); - expect(image).toBeInTheDocument(); + const viewEventsBtn = screen.getByTestId('viewAllEvents'); + const viewPostsBtn = screen.getByTestId('viewAllPosts'); + const viewMSBtn = screen.getByTestId('viewAllMembershipRequests'); + + userEvent.click(viewEventsBtn); + userEvent.click(viewPostsBtn); + fireEvent.click(viewMSBtn); + expect(toast.success).toBeCalledWith('Coming soon!'); + + expect( + screen.getByText('No membership requests present') + ).toBeInTheDocument(); + expect(screen.getByText('No upcoming events')).toBeInTheDocument(); + expect(screen.getByText('No posts present')).toBeInTheDocument(); }); - test('Should check if organisation image is not present', async () => { - const { container } = render( - - - - - - - - - - ); - - expect(container.textContent).not.toBe('Loading data...'); + + test('Testing error scenario', async () => { + await act(async () => { + render( + + + + + + + + + + ); + }); + await wait(); - const image = screen.getByTestId(/orgDashImgAbsent/i); - expect(image).toBeInTheDocument(); + expect(window.location).toBeAt('/orglist'); }); }); diff --git a/src/screens/OrganizationDashboard/OrganizationDashboard.tsx b/src/screens/OrganizationDashboard/OrganizationDashboard.tsx index 9d31a5c535..87e538a538 100644 --- a/src/screens/OrganizationDashboard/OrganizationDashboard.tsx +++ b/src/screens/OrganizationDashboard/OrganizationDashboard.tsx @@ -1,37 +1,55 @@ -import React, { useState } from 'react'; -import Row from 'react-bootstrap/Row'; +import React, { useEffect, useState } from 'react'; +import { useQuery } from '@apollo/client'; +import { Button, Card } from 'react-bootstrap'; import Col from 'react-bootstrap/Col'; -import { useMutation, useQuery } from '@apollo/client'; -import { useSelector } from 'react-redux'; -import type { RootState } from 'state/reducers'; -import { Container, Modal } from 'react-bootstrap'; +import Row from 'react-bootstrap/Row'; import { useTranslation } from 'react-i18next'; -import Button from 'react-bootstrap/Button'; -import { Link } from 'react-router-dom'; -import styles from './OrganizationDashboard.module.css'; -import AboutImg from 'assets/images/defaultImg.png'; import { ORGANIZATIONS_LIST, ORGANIZATION_EVENT_LIST, ORGANIZATION_POST_LIST, - USER_ORGANIZATION_LIST, } from 'GraphQl/Queries/Queries'; -import { DELETE_ORGANIZATION_MUTATION } from 'GraphQl/Mutations/mutations'; -import { errorHandler } from 'utils/errorHandler'; -import Loader from 'components/Loader/Loader'; +import { ReactComponent as AdminsIcon } from 'assets/svgs/admin.svg'; +import { ReactComponent as BlockedUsersIcon } from 'assets/svgs/blockedUser.svg'; +import { ReactComponent as EventsIcon } from 'assets/svgs/events.svg'; +import { ReactComponent as PostsIcon } from 'assets/svgs/post.svg'; +import { ReactComponent as UsersIcon } from 'assets/svgs/users.svg'; +import DashBoardCard from 'components/OrganizationDashCards/DashboardCard'; import OrganizationScreen from 'components/OrganizationScreen/OrganizationScreen'; +import styles from './OrganizationDashboard.module.css'; +import CardItem from 'components/OrganizationDashCards/CardItem'; +import type { ApolloError } from '@apollo/client'; +import type { + InterfaceQueryOrganizationEventListItem, + InterfaceQueryOrganizationPostListItem, + InterfaceQueryOrganizationsListObject, +} from 'utils/interfaces'; +import { toast } from 'react-toastify'; +import { useHistory } from 'react-router-dom'; +import CardItemLoading from 'components/OrganizationDashCards/CardItemLoading'; +import DashboardCardLoading from 'components/OrganizationDashCards/DashboardCardLoading'; function organizationDashboard(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'dashboard' }); - const [showDeleteModal, setShowDeleteModal] = useState(false); document.title = t('title'); const currentUrl = window.location.href.split('=')[1]; + const history = useHistory(); + const [upcomingEvents, setUpcomingEvents] = useState< + InterfaceQueryOrganizationEventListItem[] + >([]); - const appRoutes = useSelector((state: RootState) => state.appRoutes); - const { targets } = appRoutes; - - const { data, loading, error } = useQuery(ORGANIZATIONS_LIST, { + const { + data, + loading: loadingOrgData, + error: errorOrg, + }: { + data?: { + organizations: InterfaceQueryOrganizationsListObject[]; + }; + loading: boolean; + error?: ApolloError; + } = useQuery(ORGANIZATIONS_LIST, { variables: { id: currentUrl }, }); @@ -39,6 +57,14 @@ function organizationDashboard(): JSX.Element { data: postData, loading: loadingPost, error: errorPost, + }: { + data: + | { + postsByOrganization: InterfaceQueryOrganizationPostListItem[]; + } + | undefined; + loading: boolean; + error?: ApolloError; } = useQuery(ORGANIZATION_POST_LIST, { variables: { id: currentUrl }, }); @@ -47,274 +73,218 @@ function organizationDashboard(): JSX.Element { data: eventData, loading: loadingEvent, error: errorEvent, + }: { + data: + | { + eventsByOrganization: InterfaceQueryOrganizationEventListItem[]; + } + | undefined; + loading: boolean; + error?: ApolloError; } = useQuery(ORGANIZATION_EVENT_LIST, { variables: { id: currentUrl }, }); - const { data: data2 } = useQuery(USER_ORGANIZATION_LIST, { - variables: { id: localStorage.getItem('id') }, - }); - - const canDelete = data2?.user.userType === 'SUPERADMIN'; - const toggleDeleteModal = (): void => setShowDeleteModal(!showDeleteModal); - const [del] = useMutation(DELETE_ORGANIZATION_MUTATION); - - const deleteOrg = async (): Promise => { - try { - const { data } = await del({ - variables: { - id: currentUrl, - }, + // UseEffect to update upcomingEvents array + useEffect(() => { + if (eventData && eventData?.eventsByOrganization.length > 0) { + const tempUpcomingEvents: InterfaceQueryOrganizationEventListItem[] = []; + eventData?.eventsByOrganization.map((event) => { + const startDate = new Date(event.startDate); + const now = new Date(); + if (startDate > now) { + tempUpcomingEvents.push(event); + } }); - - /* istanbul ignore next */ - if (data) { - window.location.replace('/orglist'); - } - } catch (error: any) { - /* istanbul ignore next */ - errorHandler(t, error); + setUpcomingEvents(tempUpcomingEvents); } - }; + }, [eventData?.eventsByOrganization]); - if (loading || loadingPost || loadingEvent) { - return ; - } - - /* istanbul ignore next */ - if (error || errorPost || errorEvent) { + if (errorOrg || errorPost || errorEvent) { window.location.replace('/orglist'); } - return ( <> - - -
-
-
{t('about')}
-

- {data?.organizations[0].description} -

-

- {t('location')} : {data?.organizations[0].location} -

- -

- {canDelete && ( + + + {loadingOrgData ? ( + + {[...Array(6)].map((_, index) => { + return ( + + + + ); + })} + + ) : ( + + + } + /> + + + } + /> + + + } + /> + + + } + /> + + + } + /> + + + } + /> + + + )} + + + +

+
Upcoming events
- )} -

-
-
- - - -
- -

{t('statistics')}

-
- - - { - const { name } = target; - return name == 'People'; - }) - .map((target: any) => { - return target.url; - })}`} - > -
-
- -
-
-

- {data?.organizations[0].members.length} -

-

{t('members')}

-
-
- - - - { - const { name } = target; - return name == 'People'; - }) - .map((target: any) => { - return target.url; - })}`} - > -
-
-
- -
-
-

- {data?.organizations[0].admins.length} -

-

{t('admins')}

-
-
-
- - - - { - const { name } = target; - return name == 'Posts'; - }) - .map((target: any) => { - return target.url; - })}`} - > -
-
- -
-
-

- {postData?.postsByOrganization.length} -

-

{t('posts')}

-
+
+ + {loadingEvent ? ( + [...Array(4)].map((_, index) => { + return ; + }) + ) : upcomingEvents.length == 0 ? ( +
+
No upcoming events
- - - - { - const { name } = target; - return name == 'Events'; - }) - .map((target: any) => { - return target.url; - })}`} - > -
-
- -
-
-

- {eventData?.eventsByOrganization.length} -

-

{t('events')}

-
-
- - - - { - const { name } = target; - return name == 'Block/Unblock'; - }) - .map((target: any) => { - return target.url; - })}`} + ) : ( + upcomingEvents.slice(0, 5).map((event) => { + return ( + + ); + }) + )} +
+ + + + +
+
Latest posts
+ +
+ + {loadingPost ? ( + [...Array(4)].map((_, index) => { + return ; + }) + ) : postData?.postsByOrganization?.length == 0 ? ( +
+
No posts present
-
- - + ) : ( + postData?.postsByOrganization.slice(0, 5).map((post) => { + return ( + + ); + }) + )} + + + + + + + +
+
Membership requests
+
-
+ + {loadingOrgData ? ( + [...Array(4)].map((_, index) => { + return ; + }) + ) : data?.organizations[0].membershipRequests.length == 0 ? ( +
+
No membership requests present
+
+ ) : ( + data?.organizations[0]?.membershipRequests + .slice(0, 8) + .map((request) => { + return ( + + ); + }) + )} +
+ - - -
{t('deleteOrganization')}
- -
- {t('deleteMsg')} - - - - -
); diff --git a/src/screens/OrganizationDashboard/OrganizationDashboardMocks.ts b/src/screens/OrganizationDashboard/OrganizationDashboardMocks.ts index 8335e55f91..f1672c03ca 100644 --- a/src/screens/OrganizationDashboard/OrganizationDashboardMocks.ts +++ b/src/screens/OrganizationDashboard/OrganizationDashboardMocks.ts @@ -1,11 +1,11 @@ -import { DELETE_ORGANIZATION_MUTATION } from 'GraphQl/Mutations/mutations'; import { ORGANIZATIONS_LIST, ORGANIZATION_EVENT_LIST, ORGANIZATION_POST_LIST, } from 'GraphQl/Queries/Queries'; +import dayjs from 'dayjs'; -export const MOCKS_WITHOUT_IMAGE = [ +export const MOCKS = [ { request: { query: ORGANIZATIONS_LIST, @@ -14,58 +14,53 @@ export const MOCKS_WITHOUT_IMAGE = [ data: { organizations: [ { - _id: 1, + _id: 123, image: '', name: 'Dummy Organization', description: 'This is a Dummy Organization', + location: 'New Delhi', + isPublic: true, + visibleInSearch: false, creator: { firstName: '', lastName: '', email: '', }, - location: 'New Delhi', - members: { - _id: '123', - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - }, - admins: { - _id: '123', - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - }, - membershipRequests: { - _id: '456', - user: { - firstName: 'Sam', - lastName: 'Smith', - email: 'samsmith@gmail.com', + + members: [ + { + _id: '123', + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', }, - }, - blockedUsers: { - _id: '789', - firstName: 'Steve', - lastName: 'Smith', - email: 'stevesmith@gmail.com', - }, - spamCount: [ + ], + admins: [ + { + _id: '123', + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', + }, + ], + membershipRequests: [ { - _id: '6954', + _id: '456', user: { - _id: '878', - firstName: 'Joe', - lastName: 'Root', - email: 'joeroot@gmail.com', - }, - isReaded: false, - groupchat: { - _id: '321', - title: 'Dummy', + firstName: 'Jane', + lastName: 'Doe', + email: 'janedoe@gmail.com', }, }, ], + blockedUsers: [ + { + _id: '789', + firstName: 'Steve', + lastName: 'Smith', + email: 'stevesmith@gmail.com', + }, + ], }, ], }, @@ -80,8 +75,8 @@ export const MOCKS_WITHOUT_IMAGE = [ postsByOrganization: [ { _id: 1, - title: 'Akatsuki', - text: 'Capture Jinchuriki', + title: 'Post 1', + text: 'Test Post', imageUrl: '', videoUrl: '', creator: { @@ -95,20 +90,6 @@ export const MOCKS_WITHOUT_IMAGE = [ }, }, }, - { - request: { - query: DELETE_ORGANIZATION_MUTATION, - }, - result: { - data: { - removeOrganization: [ - { - _id: 1, - }, - ], - }, - }, - }, { request: { query: ORGANIZATION_EVENT_LIST, @@ -118,14 +99,28 @@ export const MOCKS_WITHOUT_IMAGE = [ eventsByOrganization: [ { _id: 1, - title: 'Event', + title: 'Event 1', description: 'Event Test', - startDate: '', - endDate: '', + startDate: dayjs(new Date()).add(1, 'day'), + endDate: dayjs(new Date()).add(3, 'day'), location: 'New Delhi', - startTime: '02:00', - endTime: '06:00', - allDay: false, + startTime: '', + endTime: '', + allDay: true, + recurring: false, + isPublic: true, + isRegisterable: true, + }, + { + _id: 2, + title: 'Event 2', + description: 'Event Test', + startDate: dayjs(new Date()), + endDate: dayjs(new Date()).add(1, 'day'), + location: 'Jamaica', + startTime: '', + endTime: '', + allDay: true, recurring: false, isPublic: true, isRegisterable: true, @@ -136,7 +131,7 @@ export const MOCKS_WITHOUT_IMAGE = [ }, ]; -export const MOCKS_NO_TAGS = [ +export const EMPTY_MOCKS = [ { request: { query: ORGANIZATIONS_LIST, @@ -145,56 +140,41 @@ export const MOCKS_NO_TAGS = [ data: { organizations: [ { - _id: 1, + _id: 123, image: '', name: 'Dummy Organization', description: 'This is a Dummy Organization', - creator: { - firstName: '', - lastName: '', - email: '', - }, location: 'New Delhi', - members: { - _id: '123', - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - }, - admins: { - _id: '123', + isPublic: true, + visibleInSearch: false, + creator: { firstName: 'John', lastName: 'Doe', email: 'johndoe@gmail.com', }, - membershipRequests: { - _id: '456', - user: { - firstName: 'Sam', - lastName: 'Smith', - email: 'samsmith@gmail.com', + members: [ + { + _id: '123', + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', }, - }, - blockedUsers: { - _id: '789', - firstName: 'Steve', - lastName: 'Smith', - email: 'stevesmith@gmail.com', - }, - spamCount: [ + ], + admins: [ { - _id: '6954', - user: { - _id: '878', - firstName: 'Joe', - lastName: 'Root', - email: 'joeroot@gmail.com', - }, - isReaded: false, - groupchat: { - _id: '321', - title: 'Dummy', - }, + _id: '123', + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', + }, + ], + membershipRequests: [], + blockedUsers: [ + { + _id: '789', + firstName: 'Steve', + lastName: 'Smith', + email: 'stevesmith@gmail.com', }, ], }, @@ -208,21 +188,7 @@ export const MOCKS_NO_TAGS = [ }, result: { data: { - postsByOrganization: [ - { - _id: 1, - title: 'Akatsuki', - text: 'Capture Jinchuriki', - imageUrl: '', - videoUrl: '', - creator: { - _id: '583', - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - }, - }, - ], + postsByOrganization: [], }, }, }, @@ -232,140 +198,29 @@ export const MOCKS_NO_TAGS = [ }, result: { data: { - eventsByOrganization: [ - { - _id: 1, - title: 'Event', - description: 'Event Test', - startDate: '', - endDate: '', - location: 'New Delhi', - startTime: '02:00', - endTime: '06:00', - allDay: false, - recurring: false, - isPublic: true, - isRegisterable: true, - }, - ], + eventsByOrganization: [], }, }, }, ]; -export const MOCKS_WITH_IMAGE = [ +export const ERROR_MOCKS = [ { request: { query: ORGANIZATIONS_LIST, }, - result: { - data: { - organizations: [ - { - _id: 1, - image: 'https://via.placeholder.com/200x200', - name: 'Dummy Organization', - description: 'This is a Dummy Organization', - creator: { - firstName: '', - lastName: '', - email: '', - }, - location: 'New Delhi', - members: { - _id: '123', - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - }, - admins: { - _id: '123', - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - }, - membershipRequests: { - _id: '456', - user: { - firstName: 'Sam', - lastName: 'Smith', - email: 'samsmith@gmail.com', - }, - }, - blockedUsers: { - _id: '789', - firstName: 'Steve', - lastName: 'Smith', - email: 'stevesmith@gmail.com', - }, - spamCount: [ - { - _id: '6954', - user: { - _id: '878', - firstName: 'Joe', - lastName: 'Root', - email: 'joeroot@gmail.com', - }, - isReaded: false, - groupchat: { - _id: '321', - title: 'Dummy', - }, - }, - ], - }, - ], - }, - }, + error: new Error('Mock Graphql ORGANIZATIONS_LIST Error'), }, { request: { query: ORGANIZATION_POST_LIST, }, - result: { - data: { - postsByOrganization: [ - { - _id: 1, - title: 'Akatsuki', - text: 'Capture Jinchuriki', - imageUrl: '', - videoUrl: '', - creator: { - _id: '583', - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - }, - }, - ], - }, - }, + error: new Error('Mock Graphql ORGANIZATION_POST_LIST Error'), }, { request: { query: ORGANIZATION_EVENT_LIST, }, - result: { - data: { - eventsByOrganization: [ - { - _id: 1, - title: 'Event', - description: 'Event Test', - startDate: '', - endDate: '', - location: 'New Delhi', - startTime: '02:00', - endTime: '06:00', - allDay: false, - recurring: false, - isPublic: true, - isRegisterable: true, - }, - ], - }, - }, + error: new Error('Mock Graphql ORGANIZATION_EVENT_LIST Error'), }, ]; diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 44994640f8..304fc36715 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -46,6 +46,8 @@ export interface InterfaceQueryOrganizationsListObject { name: string; description: string; location: string; + isPublic: boolean; + visibleInSearch: boolean; members: { _id: string; firstName: string; @@ -73,3 +75,41 @@ export interface InterfaceQueryOrganizationsListObject { email: string; }[]; } + +export interface InterfaceQueryOrganizationPostListItem { + _id: string; + title: string; + text: string; + imageUrl: null; + videoUrl: null; + creator: { + _id: string; + firstName: string; + lastName: string; + email: string; + }[]; +} +export interface InterfaceQueryOrganizationEventListItem { + _id: string; + title: string; + description: string; + startDate: string; + endDate: string; + location: string; + startTime: string; + endTime: string; + allDay: boolean; + recurring: boolean; + isPublic: boolean; + isRegisterable: boolean; +} + +export interface InterfaceQueryBlockPageMemberListItem { + _id: string; + firstName: string; + lastName: string; + email: string; + organizationsBlockedBy: { + _id: string; + }[]; +} From 9a3e663350df5d1c5b0be1ebecb534de0c387dec Mon Sep 17 00:00:00 2001 From: Noble Mittal <62551163+beingnoble03@users.noreply.github.com> Date: Thu, 21 Sep 2023 20:31:52 +0530 Subject: [PATCH 3/4] Events screen and Post Comment Modal for Talawa-user-portal (#963) * Create login and register page for user portal * Lint public locales * Create tests for login page components * Add react import in tests * Create organizations screen for user portal * Fix failing tests and lint code * fix failing tests * Add tests for organization screen and componenets * Fix non-null assertions * Fix non-null assertions in organizations test * Fix bootstrap migration changes * Add Home Screen along with other components * Fix failing tests * Add required tests and Offcanvas navbar * Remove unused variables from tests * Sync the Mutations with talawa-api * Add Settings and Donate Screen * Add multilingual support for the screens * Add events screen without calendar view * Add multilingual support to Events screen * Fix failing tests due to merge * Add Post Comment functionality * Refactor Events tests * Deprecate event registrants from query --- package-lock.json | 8 +- public/locales/en.json | 29 ++ public/locales/fr.json | 29 ++ public/locales/hi.json | 29 ++ public/locales/sp.json | 29 ++ public/locales/zh.json | 29 ++ src/App.tsx | 2 + src/GraphQl/Mutations/mutations.ts | 43 ++ src/GraphQl/Queries/Queries.ts | 57 ++ .../CommentCard/CommentCard.module.css | 46 ++ .../CommentCard/CommentCard.test.tsx | 228 ++++++++ .../UserPortal/CommentCard/CommentCard.tsx | 101 ++++ .../UserPortal/EventCard/EventCard.module.css | 26 + .../UserPortal/EventCard/EventCard.tsx | 124 +++++ .../OrganizationSidebar.tsx | 6 +- .../UserPortal/PostCard/PostCard.module.css | 26 + .../UserPortal/PostCard/PostCard.test.tsx | 145 +++++- .../UserPortal/PostCard/PostCard.tsx | 245 +++++++-- .../UserPortal/UserSidebar/UserSidebar.tsx | 28 +- src/screens/OrgPost/OrgPost.test.tsx | 3 + .../UserPortal/Events/Events.module.css | 83 +++ src/screens/UserPortal/Events/Events.test.tsx | 473 +++++++++++++++++ src/screens/UserPortal/Events/Events.tsx | 485 ++++++++++++++++++ src/screens/UserPortal/Home/Home.test.tsx | 37 +- src/screens/UserPortal/Home/Home.tsx | 41 ++ 25 files changed, 2290 insertions(+), 62 deletions(-) create mode 100644 src/components/UserPortal/CommentCard/CommentCard.module.css create mode 100644 src/components/UserPortal/CommentCard/CommentCard.test.tsx create mode 100644 src/components/UserPortal/CommentCard/CommentCard.tsx create mode 100644 src/components/UserPortal/EventCard/EventCard.module.css create mode 100644 src/components/UserPortal/EventCard/EventCard.tsx create mode 100644 src/screens/UserPortal/Events/Events.module.css create mode 100644 src/screens/UserPortal/Events/Events.test.tsx create mode 100644 src/screens/UserPortal/Events/Events.tsx diff --git a/package-lock.json b/package-lock.json index d8a291c684..f2cf6800e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,7 @@ "redux": "^4.1.1", "redux-thunk": "^2.3.0", "typedoc": "^0.24.8", - "typedoc-plugin-markdown": "^3.15.4", + "typedoc-plugin-markdown": "^3.16.0", "typescript": "^4.3.5", "web-vitals": "^1.0.1", "yarn": "^1.22.17" @@ -23521,9 +23521,9 @@ } }, "node_modules/typedoc-plugin-markdown": { - "version": "3.15.4", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.15.4.tgz", - "integrity": "sha512-KpjFL/NDrQAbY147oIoOgob2vAdEchsMcTVd6+e6H2lC1l5xhi48bhP/fMJI7qYQ8th5nubervgqw51z7gY66A==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.16.0.tgz", + "integrity": "sha512-eeiC78fDNGFwemPIHiwRC+mEC7W5jwt3fceUev2gJ2nFnXpVHo8eRrpC9BLWZDee6ehnz/sPmNjizbXwpfaTBw==", "dependencies": { "handlebars": "^4.7.7" }, diff --git a/public/locales/en.json b/public/locales/en.json index 6dd9156052..7f6771f1e9 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -565,5 +565,34 @@ "yourPreviousDonations": "Your Previous Donations", "donate": "Donate", "nothingToShow": "Nothing to show here." + }, + "userEvents": { + "nothingToShow": "Nothing to show here.", + "search": "Search", + "createEvent": "Create Event", + "eventTitle": "Event Title", + "eventDescription": "Event Description", + "eventLocation": "Event Location", + "startDate": "Select Start Date", + "endDate": "Select End Date", + "publicEvent": "Public Event", + "registerable": "Registerable", + "recurring": "Recurring", + "startTime": "Start Time", + "endTime": "End Time", + "cancel": "Cancel", + "create": "Create", + "listView": "List View", + "calendarView": "Calendar View", + "allDay": "All Day", + "eventCreated": "Event created and posted successfully." + }, + "userEventCard": { + "location": "Location", + "starts": "Starts", + "ends": "Ends", + "creator": "Creator", + "alreadyRegistered": "Already registered", + "register": "Register" } } diff --git a/public/locales/fr.json b/public/locales/fr.json index 78d3fade9b..786c133520 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -558,5 +558,34 @@ "yourPreviousDonations": "Vos dons précédents", "donate": "Donner", "nothingToShow": "Rien à montrer ici." + }, + "userEvents": { + "nothingToShow": "Rien à montrer ici.", + "search": "Recherche", + "createEvent": "Créer un évènement", + "eventTitle": "Titre de l'événement", + "eventDescription": "Description de l'évenement", + "eventLocation": "Lieu de l'événement", + "startDate": "Sélectionnez la date de début", + "endDate": "Sélectionnez la date de fin", + "publicEvent": "Évennement publique", + "registerable": "Enregistrable", + "recurring": "Récurrente", + "startTime": "Heure de début", + "endTime": "Heure de fin", + "cancel": "Annuler", + "create": "Créer", + "listView": "Vue en liste", + "calendarView": "Vue du calendrier", + "allDay": "Toute la journée", + "eventCreated": "Événement créé et publié avec succès." + }, + "userEventCard": { + "location": "Emplacement", + "starts": "Départs", + "ends": "Prend fin", + "creator": "Créatrice", + "alreadyRegistered": "Déjà enregistré", + "register": "Registre" } } diff --git a/public/locales/hi.json b/public/locales/hi.json index 29b449a0c9..5f5654b884 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -559,5 +559,34 @@ "yourPreviousDonations": "आपका पिछला दान", "donate": "दान", "nothingToShow": "यहां दिखाने के लिए कुछ भी नहीं है." + }, + "userEvents": { + "nothingToShow": "यहां दिखाने के लिए कुछ भी नहीं है.", + "search": "खोज", + "createEvent": "कार्यक्रम बनाएँ", + "eventTitle": "कार्यक्रम का शीर्षक", + "eventDescription": "घटना विवरण", + "eventLocation": "घटना स्थान", + "startDate": "आरंभ तिथि चुनें", + "endDate": "अंतिम तिथि चुनें", + "publicEvent": "सार्वजनिक समारोह", + "registerable": "पंजीकरण योग्य", + "recurring": "पुनरावर्ती", + "startTime": "समय शुरू", + "endTime": "अंत समय", + "cancel": "रद्द करना", + "create": "बनाएं", + "listView": "लिस्ट व्यू", + "calendarView": "कैलेंडर दृश्य", + "allDay": "पूरे दिन", + "eventCreated": "ईवेंट सफलतापूर्वक बनाया और पोस्ट किया गया." + }, + "userEventCard": { + "location": "जगह", + "starts": "प्रारंभ होगा", + "ends": "समाप्त होगा", + "creator": "निर्माता", + "alreadyRegistered": "पहले से ही पंजीकृत", + "register": "पंजीकरण करवाना" } } diff --git a/public/locales/sp.json b/public/locales/sp.json index 7fa6ccd04b..85f3492c02 100644 --- a/public/locales/sp.json +++ b/public/locales/sp.json @@ -559,5 +559,34 @@ "yourPreviousDonations": "Tus donaciones anteriores", "donate": "Donar", "nothingToShow": "Nada que mostrar aquí." + }, + "userEvents": { + "nothingToShow": "No hay nada que mostrar aquí.", + "search": "Buscar", + "createEvent": "Crear evento", + "eventTitle": "Título del evento", + "eventDescription": "Descripción del evento", + "eventLocation": "Lugar del evento", + "startDate": "Seleccione la fecha de inicio", + "endDate": "Seleccionar fecha de finalización", + "publicEvent": "Evento público", + "registerable": "Registrable", + "recurring": "Periódica", + "startTime": "Hora de inicio", + "endTime": "Hora de finalización", + "cancel": "Cancelar", + "create": "Crear", + "listView": "Vista de la lista", + "calendarView": "Vista de calendario", + "allDay": "Todo el dia", + "eventCreated": "Evento creado y publicado exitosamente." + }, + "userEventCard": { + "location": "Ubicación", + "starts": "Empieza", + "ends": "Termina", + "creator": "Creadora", + "alreadyRegistered": "Ya registrado", + "register": "Registro" } } diff --git a/public/locales/zh.json b/public/locales/zh.json index a4232d43cf..055bb709b2 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -559,5 +559,34 @@ "yourPreviousDonations": "您之前的捐款", "donate": "捐", "nothingToShow": "這裡沒有什麼可顯示的。" + }, + "userEvents": { + "nothingToShow": "這裡沒有什麼可顯示的。", + "search": "搜索", + "createEvent": "創建事件", + "eventTitle": "活動標題", + "eventDescription": "活動說明", + "eventLocation": "活動地點", + "startDate": "選擇開始日期", + "endDate": "選擇結束日期", + "publicEvent": "公共活動", + "registerable": "可註冊", + "recurring": "再次發生的", + "startTime": "開始時間", + "endTime": "時間結束", + "cancel": "取消", + "create": "創造", + "listView": "列表顯示", + "calendarView": "日曆視圖", + "allDay": "整天", + "eventCreated": "活動已成功創建並發布。" + }, + "userEventCard": { + "location": "地點", + "starts": "開始", + "ends": "結束", + "creator": "創作者", + "alreadyRegistered": "已經註冊", + "register": "登記" } } diff --git a/src/App.tsx b/src/App.tsx index 517b6eebc7..31e97bdc64 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,6 +30,7 @@ import Home from 'screens/UserPortal/Home/Home'; import People from 'screens/UserPortal/People/People'; import Settings from 'screens/UserPortal/Settings/Settings'; import Donate from 'screens/UserPortal/Donate/Donate'; +import Events from 'screens/UserPortal/Events/Events'; function app(): JSX.Element { /*const { updatePluginLinks, updateInstalled } = bindActionCreators( @@ -123,6 +124,7 @@ function app(): JSX.Element { + diff --git a/src/GraphQl/Mutations/mutations.ts b/src/GraphQl/Mutations/mutations.ts index 550ad1e07d..be487f0d15 100644 --- a/src/GraphQl/Mutations/mutations.ts +++ b/src/GraphQl/Mutations/mutations.ts @@ -457,6 +457,14 @@ export const UNLIKE_POST = gql` } `; +export const REGISTER_EVENT = gql` + mutation registerForEvent($eventId: ID!) { + registerForEvent(id: $eventId) { + _id + } + } +`; + export const ADD_EVENT_PROJECT_MUTATION = gql` mutation AddEventProject( $title: String! @@ -579,3 +587,38 @@ export const MARK_CHECKIN = gql` } } `; + +export const CREATE_COMMENT_POST = gql` + mutation createComment($comment: String!, $postId: ID!) { + createComment(data: { text: $comment }, postId: $postId) { + _id + creator { + _id + firstName + lastName + email + } + likeCount + likedBy { + _id + } + text + } + } +`; + +export const LIKE_COMMENT = gql` + mutation likeComment($commentId: ID!) { + likeComment(id: $commentId) { + _id + } + } +`; + +export const UNLIKE_COMMENT = gql` + mutation unlikeComment($commentId: ID!) { + unlikeComment(id: $commentId) { + _id + } + } +`; diff --git a/src/GraphQl/Queries/Queries.ts b/src/GraphQl/Queries/Queries.ts index 6efae00ed5..ef8aa20a2b 100644 --- a/src/GraphQl/Queries/Queries.ts +++ b/src/GraphQl/Queries/Queries.ts @@ -514,6 +514,20 @@ export const ORGANIZATION_POST_CONNECTION_LIST = gql` } likeCount commentCount + comments { + _id + creator { + _id + firstName + lastName + email + } + likeCount + likedBy { + _id + } + text + } likedBy { _id firstName @@ -606,3 +620,46 @@ export const PLUGIN_GET = gql` } } `; + +export const ORGANIZATION_EVENTS_CONNECTION = gql` + query EventsByOrganizationConnection( + $organization_id: ID! + $title_contains: String + $description_contains: String + $location_contains: String + $first: Int + $skip: Int + ) { + eventsByOrganizationConnection( + where: { + organization_id: $organization_id + title_contains: $title_contains + description_contains: $description_contains + location_contains: $location_contains + } + first: $first + skip: $skip + ) { + _id + title + description + startDate + endDate + location + startTime + endTime + allDay + recurring + isPublic + isRegisterable + creator { + _id + firstName + lastName + } + attendees { + _id + } + } + } +`; diff --git a/src/components/UserPortal/CommentCard/CommentCard.module.css b/src/components/UserPortal/CommentCard/CommentCard.module.css new file mode 100644 index 0000000000..1124f6369d --- /dev/null +++ b/src/components/UserPortal/CommentCard/CommentCard.module.css @@ -0,0 +1,46 @@ +.mainContainer { + width: 100%; + display: flex; + flex-direction: row; + padding: 10px; + background-color: white; + border-radius: 10px; + box-shadow: 2px 2px 8px 0px #c8c8c8; + overflow: hidden; + margin-top: 10px; +} + +.personDetails { + display: flex; + flex-direction: column; + justify-content: center; +} + +.personImage { + border-radius: 50%; + margin-right: 20px; +} + +.cardActions { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; + margin-top: 10px; +} + +.cardActionBtn { + background-color: rgba(0, 0, 0, 0); + border: none; + color: black; +} + +.cardActionBtn:hover { + background-color: ghostwhite; + border: none; + color: green !important; +} + +.likeIcon { + width: 20px; +} diff --git a/src/components/UserPortal/CommentCard/CommentCard.test.tsx b/src/components/UserPortal/CommentCard/CommentCard.test.tsx new file mode 100644 index 0000000000..c586809132 --- /dev/null +++ b/src/components/UserPortal/CommentCard/CommentCard.test.tsx @@ -0,0 +1,228 @@ +import React from 'react'; +import { MockedProvider } from '@apollo/react-testing'; +import { act, render, screen } from '@testing-library/react'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import { store } from 'state/store'; +import i18nForTest from 'utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; + +import CommentCard from './CommentCard'; +import userEvent from '@testing-library/user-event'; +import { LIKE_COMMENT, UNLIKE_COMMENT } from 'GraphQl/Mutations/mutations'; + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +const MOCKS = [ + { + request: { + query: LIKE_COMMENT, + variables: { + commentId: '1', + }, + result: { + data: { + likeComment: { + _id: '1', + }, + }, + }, + }, + }, + { + request: { + query: UNLIKE_COMMENT, + variables: { + commentId: '1', + }, + result: { + data: { + unlikeComment: { + _id: '1', + }, + }, + }, + }, + }, +]; + +const link = new StaticMockLink(MOCKS, true); + +describe('Testing CommentCard Component [User Portal]', () => { + afterEach(async () => { + await act(async () => { + await i18nForTest.changeLanguage('en'); + }); + }); + + test('Component should be rendered properly if comment is already liked by the user.', async () => { + const cardProps = { + id: '1', + creator: { + id: '1', + firstName: 'test', + lastName: 'user', + email: 'test@user.com', + }, + likeCount: 1, + likedBy: [ + { + id: '1', + }, + ], + text: 'testComment', + }; + + const beforeUserId = localStorage.getItem('userId'); + localStorage.setItem('userId', '2'); + + render( + + + + + + + + + + ); + + await wait(); + if (beforeUserId) { + localStorage.setItem('userId', beforeUserId); + } + }); + + test('Component should be rendered properly if comment is not already liked by the user.', async () => { + const cardProps = { + id: '1', + creator: { + id: '1', + firstName: 'test', + lastName: 'user', + email: 'test@user.com', + }, + likeCount: 1, + likedBy: [ + { + id: '1', + }, + ], + text: 'testComment', + }; + + const beforeUserId = localStorage.getItem('userId'); + localStorage.setItem('userId', '1'); + + render( + + + + + + + + + + ); + + await wait(); + if (beforeUserId) { + localStorage.setItem('userId', beforeUserId); + } + }); + + test('Component renders as expected if user likes the comment.', async () => { + const cardProps = { + id: '1', + creator: { + id: '1', + firstName: 'test', + lastName: 'user', + email: 'test@user.com', + }, + likeCount: 1, + likedBy: [ + { + id: '1', + }, + ], + text: 'testComment', + }; + + const beforeUserId = localStorage.getItem('userId'); + localStorage.setItem('userId', '2'); + + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('likeCommentBtn')); + + await wait(); + + if (beforeUserId) { + localStorage.setItem('userId', beforeUserId); + } + }); + + test('Component renders as expected if user unlikes the comment.', async () => { + const cardProps = { + id: '1', + creator: { + id: '1', + firstName: 'test', + lastName: 'user', + email: 'test@user.com', + }, + likeCount: 1, + likedBy: [ + { + id: '1', + }, + ], + text: 'testComment', + }; + + const beforeUserId = localStorage.getItem('userId'); + localStorage.setItem('userId', '1'); + + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('likeCommentBtn')); + + if (beforeUserId) { + localStorage.setItem('userId', beforeUserId); + } + }); +}); diff --git a/src/components/UserPortal/CommentCard/CommentCard.tsx b/src/components/UserPortal/CommentCard/CommentCard.tsx new file mode 100644 index 0000000000..8b2d5a8fca --- /dev/null +++ b/src/components/UserPortal/CommentCard/CommentCard.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { Button } from 'react-bootstrap'; +import styles from './CommentCard.module.css'; +import ThumbUpIcon from '@mui/icons-material/ThumbUp'; +import { useMutation } from '@apollo/client'; +import { LIKE_COMMENT, UNLIKE_COMMENT } from 'GraphQl/Mutations/mutations'; +import { toast } from 'react-toastify'; +import HourglassBottomIcon from '@mui/icons-material/HourglassBottom'; +import ThumbUpOffAltIcon from '@mui/icons-material/ThumbUpOffAlt'; + +interface InterfaceCommentCardProps { + id: string; + creator: { + id: string; + firstName: string; + lastName: string; + email: string; + }; + likeCount: number; + likedBy: { + id: string; + }[]; + text: string; +} + +function commentCard(props: InterfaceCommentCardProps): JSX.Element { + const creatorName = `${props.creator.firstName} ${props.creator.lastName}`; + + const userId = localStorage.getItem('userId'); + const likedByUser = props.likedBy.some((likedBy) => likedBy.id === userId); + + const [likes, setLikes] = React.useState(props.likeCount); + const [isLikedByUser, setIsLikedByUser] = React.useState(likedByUser); + const [likeComment, { loading: likeLoading }] = useMutation(LIKE_COMMENT); + const [unlikeComment, { loading: unlikeLoading }] = + useMutation(UNLIKE_COMMENT); + + const handleToggleLike = async (): Promise => { + if (isLikedByUser) { + try { + const { data } = await unlikeComment({ + variables: { + commentId: props.id, + }, + }); + /* istanbul ignore next */ + if (data) { + setLikes((likes) => likes - 1); + setIsLikedByUser(false); + } + } catch (error: any) { + /* istanbul ignore next */ + toast.error(error); + } + } else { + try { + const { data } = await likeComment({ + variables: { + commentId: props.id, + }, + }); + /* istanbul ignore next */ + if (data) { + setLikes((likes) => likes + 1); + setIsLikedByUser(true); + } + } catch (error: any) { + /* istanbul ignore next */ + toast.error(error); + } + } + }; + + return ( +
+
+ {creatorName} + {props.text} +
+ + {`${likes} Likes`} +
+
+
+ ); +} + +export default commentCard; diff --git a/src/components/UserPortal/EventCard/EventCard.module.css b/src/components/UserPortal/EventCard/EventCard.module.css new file mode 100644 index 0000000000..28278dd5a6 --- /dev/null +++ b/src/components/UserPortal/EventCard/EventCard.module.css @@ -0,0 +1,26 @@ +.mainContainer { + width: 100%; + display: flex; + flex-direction: column; + padding: 10px; + cursor: pointer; + background-color: white; + border-radius: 10px; + box-shadow: 2px 2px 8px 0px #c8c8c8; + overflow: hidden; +} + +.eventDetails { + gap: 5px; +} + +.personImage { + border-radius: 50%; + margin-right: 20px; +} + +.eventActions { + display: flex; + flex-direction: row; + justify-content: right; +} diff --git a/src/components/UserPortal/EventCard/EventCard.tsx b/src/components/UserPortal/EventCard/EventCard.tsx new file mode 100644 index 0000000000..5aac562c06 --- /dev/null +++ b/src/components/UserPortal/EventCard/EventCard.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import styles from './EventCard.module.css'; +import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; +import dayjs from 'dayjs'; +import { Button } from 'react-bootstrap'; +import { useMutation } from '@apollo/client'; +import { toast } from 'react-toastify'; +import HourglassBottomIcon from '@mui/icons-material/HourglassBottom'; + +import { REGISTER_EVENT } from 'GraphQl/Mutations/mutations'; +import { useTranslation } from 'react-i18next'; + +interface InterfaceEventCardProps { + id: string; + title: string; + description: string; + location: string; + startDate: string; + endDate: string; + isRegisterable: boolean; + isPublic: boolean; + endTime: string; + startTime: string; + recurring: boolean; + allDay: boolean; + creator: { + firstName: string; + lastName: string; + id: string; + }; + registrants: { + id: string; + }[]; +} + +function eventCard(props: InterfaceEventCardProps): JSX.Element { + const { t } = useTranslation('translation', { + keyPrefix: 'userEventCard', + }); + const userId = localStorage.getItem('userId'); + const creatorName = `${props.creator.firstName} ${props.creator.lastName}`; + const isInitiallyRegistered = props.registrants.some( + (registrant) => registrant.id === userId + ); + + const [registerEventMutation, { loading }] = useMutation(REGISTER_EVENT); + const [isRegistered, setIsRegistered] = React.useState(isInitiallyRegistered); + + const handleRegister = async (): Promise => { + if (!isRegistered) { + try { + const { data } = await registerEventMutation({ + variables: { + eventId: props.id, + }, + }); + /* istanbul ignore next */ + if (data) { + setIsRegistered(true); + toast.success(`Successfully registered for ${props.title}`); + } + } catch (error: any) { + /* istanbul ignore next */ + toast.error(error); + } + } + }; + + return ( +
+
+
+ {props.title} +
+
+ +
+
+ {props.description} + + {`${t('location')} `} + {props.location} + +
+ {`${t('starts')} `} + {props.startTime ? ( + {dayjs(`2015-03-04T${props.startTime}`).format('h:mm:ss A')} + ) : ( + <> + )} + {dayjs(props.startDate).format("D MMMM 'YY")} +
+
+ {`${t('ends')} `} + {props.endTime ? ( + {dayjs(`2015-03-04T${props.endTime}`).format('h:mm:ss A')} + ) : ( + <> + )}{' '} + {dayjs(props.endDate).format("D MMMM 'YY")} +
+ + {`${t('creator')} `} + {creatorName} + + +
+ {loading ? ( + + ) : isRegistered ? ( + + ) : ( + + )} +
+
+ ); +} + +export default eventCard; diff --git a/src/components/UserPortal/OrganizationSidebar/OrganizationSidebar.tsx b/src/components/UserPortal/OrganizationSidebar/OrganizationSidebar.tsx index b45a1892d5..570ea9de85 100644 --- a/src/components/UserPortal/OrganizationSidebar/OrganizationSidebar.tsx +++ b/src/components/UserPortal/OrganizationSidebar/OrganizationSidebar.tsx @@ -23,6 +23,8 @@ export default function organizationSidebar(): JSX.Element { const organizationId = getOrganizationId(window.location.href); const [members, setMembers]: any = React.useState([]); const [events, setEvents]: any = React.useState([]); + const eventsLink = `/user/events/id=${organizationId}`; + const peopleLink = `/user/people/id=${organizationId}`; const { data: memberData, loading: memberLoading } = useQuery( ORGANIZATIONS_MEMBER_CONNECTION_LIST, @@ -99,7 +101,7 @@ export default function organizationSidebar(): JSX.Element { )}
- + {t('viewAll')} @@ -145,7 +147,7 @@ export default function organizationSidebar(): JSX.Element { )}
- + {t('viewAll')} diff --git a/src/components/UserPortal/PostCard/PostCard.module.css b/src/components/UserPortal/PostCard/PostCard.module.css index 1cab3b2abd..56158473f9 100644 --- a/src/components/UserPortal/PostCard/PostCard.module.css +++ b/src/components/UserPortal/PostCard/PostCard.module.css @@ -27,3 +27,29 @@ gap: 10px; align-items: center; } + +.creatorNameModal { + display: flex; + flex-direction: row; + gap: 5px; + align-items: center; + margin-bottom: 10px; +} + +.modalActions { + display: flex; + flex-direction: row; + align-items: center; + gap: 1px; + margin: 5px 0px; +} + +.textModal { + margin-top: 10px; +} + +.colorPrimary { + background: #31bb6b; + color: white; + cursor: pointer; +} diff --git a/src/components/UserPortal/PostCard/PostCard.test.tsx b/src/components/UserPortal/PostCard/PostCard.test.tsx index 3bdf488fb6..acd6176b02 100644 --- a/src/components/UserPortal/PostCard/PostCard.test.tsx +++ b/src/components/UserPortal/PostCard/PostCard.test.tsx @@ -10,7 +10,11 @@ import { StaticMockLink } from 'utils/StaticMockLink'; import PostCard from './PostCard'; import userEvent from '@testing-library/user-event'; -import { LIKE_POST, UNLIKE_POST } from 'GraphQl/Mutations/mutations'; +import { + CREATE_COMMENT_POST, + LIKE_POST, + UNLIKE_POST, +} from 'GraphQl/Mutations/mutations'; const MOCKS = [ { @@ -43,6 +47,33 @@ const MOCKS = [ }, }, }, + { + request: { + query: CREATE_COMMENT_POST, + variables: { + postId: '1', + comment: 'testComment', + }, + result: { + data: { + createComment: { + _id: '64ef885bca85de60ebe0f304', + creator: { + _id: '63d6064458fce20ee25c3bf7', + firstName: 'Noble', + lastName: 'Mittal', + email: 'test@gmail.com', + __typename: 'User', + }, + likeCount: 0, + likedBy: [], + text: 'testComment', + __typename: 'Comment', + }, + }, + }, + }, + }, ]; async function wait(ms = 100): Promise { @@ -70,7 +101,23 @@ describe('Testing PostCard Component [User Portal]', () => { text: 'This is post test text', title: 'This is post test title', likeCount: 1, - commentCount: 0, + commentCount: 1, + comments: [ + { + _id: '64eb13beca85de60ebe0ed0e', + creator: { + _id: '63d6064458fce20ee25c3bf7', + firstName: 'Noble', + lastName: 'Mittal', + email: 'test@gmail.com', + __typename: 'User', + }, + likeCount: 0, + likedBy: [], + text: 'First comment from Talawa user portal.', + __typename: 'Comment', + }, + ], likedBy: [ { firstName: '', @@ -113,6 +160,7 @@ describe('Testing PostCard Component [User Portal]', () => { title: 'This is post test title', likeCount: 1, commentCount: 0, + comments: [], likedBy: [ { firstName: 'test', @@ -159,6 +207,7 @@ describe('Testing PostCard Component [User Portal]', () => { title: 'This is post test title', likeCount: 1, commentCount: 0, + comments: [], likedBy: [ { firstName: 'test', @@ -207,6 +256,7 @@ describe('Testing PostCard Component [User Portal]', () => { title: 'This is post test title', likeCount: 1, commentCount: 0, + comments: [], likedBy: [ { firstName: 'test', @@ -252,6 +302,7 @@ describe('Testing PostCard Component [User Portal]', () => { title: 'This is post test title', likeCount: 1, commentCount: 0, + comments: [], likedBy: [ { firstName: 'test', @@ -275,4 +326,94 @@ describe('Testing PostCard Component [User Portal]', () => { await wait(); }); + + test('Comment is created successfully after create comment button is clicked.', async () => { + const cardProps = { + id: '1', + creator: { + firstName: 'test', + lastName: 'user', + email: 'test@user.com', + id: '1', + }, + image: 'testImage', + video: '', + text: 'This is post test text', + title: 'This is post test title', + likeCount: 1, + commentCount: 0, + comments: [], + likedBy: [ + { + firstName: 'test', + lastName: 'user', + id: '1', + }, + ], + }; + + render( + + + + + + + + + + ); + + const randomComment = 'testComment'; + + userEvent.click(screen.getByTestId('showCommentsBtn')); + + userEvent.type(screen.getByTestId('commentInput'), randomComment); + userEvent.click(screen.getByTestId('createCommentBtn')); + + await wait(); + }); + + test('Comment modal pops when show comments button is clicked.', async () => { + const cardProps = { + id: '', + creator: { + firstName: 'test', + lastName: 'user', + email: 'test@user.com', + id: '1', + }, + image: 'testImage', + video: '', + text: 'This is post test text', + title: 'This is post test title', + likeCount: 1, + commentCount: 0, + comments: [], + likedBy: [ + { + firstName: 'test', + lastName: 'user', + id: '1', + }, + ], + }; + + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('showCommentsBtn')); + expect(screen.findAllByText('Comments')).not.toBeNull(); + }); }); diff --git a/src/components/UserPortal/PostCard/PostCard.tsx b/src/components/UserPortal/PostCard/PostCard.tsx index dacc30a65b..0374858f23 100644 --- a/src/components/UserPortal/PostCard/PostCard.tsx +++ b/src/components/UserPortal/PostCard/PostCard.tsx @@ -1,15 +1,22 @@ import React from 'react'; -import { Button, Card } from 'react-bootstrap'; +import { Button, Card, Form, InputGroup, Modal } from 'react-bootstrap'; import ThumbUpIcon from '@mui/icons-material/ThumbUp'; import CommentIcon from '@mui/icons-material/Comment'; import AccountCircleIcon from '@mui/icons-material/AccountCircle'; import styles from './PostCard.module.css'; import { useMutation } from '@apollo/client'; -import { LIKE_POST, UNLIKE_POST } from 'GraphQl/Mutations/mutations'; +import { + CREATE_COMMENT_POST, + LIKE_POST, + UNLIKE_POST, +} from 'GraphQl/Mutations/mutations'; import { toast } from 'react-toastify'; import HourglassBottomIcon from '@mui/icons-material/HourglassBottom'; import ThumbUpOffAltIcon from '@mui/icons-material/ThumbUpOffAlt'; import { useTranslation } from 'react-i18next'; +import SendIcon from '@mui/icons-material/Send'; +import { errorHandler } from 'utils/errorHandler'; +import CommentCard from '../CommentCard/CommentCard'; interface InterfacePostCardProps { id: string; @@ -25,6 +32,19 @@ interface InterfacePostCardProps { title: string; likeCount: number; commentCount: number; + comments: { + creator: { + _id: string; + firstName: string; + lastName: string; + email: string; + }; + likeCount: number; + likedBy: { + id: string; + }[]; + text: string; + }[]; likedBy: { firstName: string; lastName: string; @@ -32,6 +52,21 @@ interface InterfacePostCardProps { }[]; } +interface InterfaceCommentCardProps { + id: string; + creator: { + id: string; + firstName: string; + lastName: string; + email: string; + }; + likeCount: number; + likedBy: { + id: string; + }[]; + text: string; +} + export default function postCard(props: InterfacePostCardProps): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'postCard', @@ -39,14 +74,22 @@ export default function postCard(props: InterfacePostCardProps): JSX.Element { const userId = localStorage.getItem('userId'); const likedByUser = props.likedBy.some((likedBy) => likedBy.id === userId); + const [comments, setComments] = React.useState(props.comments); + const [numComments, setNumComments] = React.useState(props.commentCount); const [likes, setLikes] = React.useState(props.likeCount); const [isLikedByUser, setIsLikedByUser] = React.useState(likedByUser); + const [showComments, setShowComments] = React.useState(false); + const [commentInput, setCommentInput] = React.useState(''); const postCreator = `${props.creator.firstName} ${props.creator.lastName}`; const [likePost, { loading: likeLoading }] = useMutation(LIKE_POST); const [unLikePost, { loading: unlikeLoading }] = useMutation(UNLIKE_POST); + const [create, { loading: commentLoading }] = + useMutation(CREATE_COMMENT_POST); + + const toggleCommentsModal = (): void => setShowComments(!showComments); const handleToggleLike = async (): Promise => { if (isLikedByUser) { @@ -84,45 +127,165 @@ export default function postCard(props: InterfacePostCardProps): JSX.Element { } }; + const handleCommentInput = ( + event: React.ChangeEvent + ): void => { + const comment = event.target.value; + setCommentInput(comment); + }; + + const createComment = async (): Promise => { + try { + const { data: createEventData } = await create({ + variables: { + postId: props.id, + comment: commentInput, + }, + }); + + /* istanbul ignore next */ + if (createEventData) { + setCommentInput(''); + setNumComments((numComments) => numComments + 1); + + const newComment: any = { + id: createEventData.createComment._id, + creator: { + id: createEventData.createComment.creator.id, + firstName: createEventData.createComment.creator.firstName, + lastName: createEventData.createComment.creator.lastName, + email: createEventData.createComment.creator.email, + }, + likeCount: createEventData.createComment.likeCount, + likedBy: createEventData.createComment.likedBy, + text: createEventData.createComment.text, + }; + + setComments([...comments, newComment]); + } + } catch (error: any) { + /* istanbul ignore next */ + errorHandler(t, error); + } + }; + return ( - - -
- - {postCreator} -
-
- - {props.title} - {props.text} - {props.image && ( - - )} - - -
- - {likes} - {` ${t('likes')}`} - - {props.commentCount} - {` ${t('comments')}`} -
-
-
+ <> + + +
+ + {postCreator} +
+
+ + {props.title} + {props.text} + {props.image && ( + + )} + + +
+ + {likes} + {` ${t('likes')}`} + + {numComments} + {` ${t('comments')}`} +
+
+
+ + +
+ + {postCreator} +
+ {props.image && ( + + )} +
{props.text}
+
+ + {likes} + {` ${t('likes')}`} +
+

Comments

+ {numComments ? ( + comments.map((comment: any, index: any) => { + const cardProps: InterfaceCommentCardProps = { + id: comment.id, + creator: { + id: comment.creator.id, + firstName: comment.creator.firstName, + lastName: comment.creator.lastName, + email: comment.creator.email, + }, + likeCount: comment.likeCount, + likedBy: comment.likedBy, + text: comment.text, + }; + + return ; + }) + ) : ( + <>No comments to show. + )} +
+ + + + {commentLoading ? ( + + ) : ( + + )} + + +
+
+ ); } diff --git a/src/components/UserPortal/UserSidebar/UserSidebar.tsx b/src/components/UserPortal/UserSidebar/UserSidebar.tsx index b122b9e229..2e455358b0 100644 --- a/src/components/UserPortal/UserSidebar/UserSidebar.tsx +++ b/src/components/UserPortal/UserSidebar/UserSidebar.tsx @@ -74,25 +74,29 @@ function userSidebar(): JSX.Element { {organizations.length ? ( organizations.map((organization: any, index) => { + const organizationUrl = `/user/organization/id=${organization._id}`; + return ( -
- -
- {organization.name} + +
+ +
+ {organization.name} +
-
+ ); }) diff --git a/src/screens/OrgPost/OrgPost.test.tsx b/src/screens/OrgPost/OrgPost.test.tsx index 478095c323..1db08a4179 100644 --- a/src/screens/OrgPost/OrgPost.test.tsx +++ b/src/screens/OrgPost/OrgPost.test.tsx @@ -43,6 +43,7 @@ const MOCKS = [ }, likeCount: 0, commentCount: 0, + comments: [], likedBy: [], }, { @@ -60,6 +61,7 @@ const MOCKS = [ likeCount: 0, commentCount: 0, likedBy: [], + comments: [], }, ], }, @@ -143,6 +145,7 @@ describe('Organisation Post Page', () => { }, likeCount: 0, commentCount: 0, + comments: [], likedBy: [], }); }); diff --git a/src/screens/UserPortal/Events/Events.module.css b/src/screens/UserPortal/Events/Events.module.css new file mode 100644 index 0000000000..52a305cc20 --- /dev/null +++ b/src/screens/UserPortal/Events/Events.module.css @@ -0,0 +1,83 @@ +.borderNone { + border: none; +} + +.colorWhite { + color: white; +} + +.backgroundWhite { + background-color: white; +} + +.maxWidth { + max-width: 300px; +} + +.colorLight { + background-color: #f5f5f5; +} + +.mainContainer { + width: 50%; + flex-grow: 3; + padding: 40px; + max-height: 100%; + overflow: auto; +} + +.content { + height: fit-content; + min-height: calc(100% - 40px); +} + +.gap { + gap: 20px; +} + +.paddingY { + padding: 30px 0px; +} + +.containerHeight { + height: calc(100vh - 66px); +} + +.colorPrimary { + background: #31bb6b; + color: white; +} + +.eventActionsContainer { + display: flex; + flex-direction: row; + gap: 15px; +} + +.datePicker { + border-radius: 10px; + height: 40px; + text-align: center; + background-color: #f2f2f2; + border: none; + width: 100%; +} + +.modalBody { + display: flex; + flex-direction: column; + gap: 10px; +} + +.switchContainer { + display: flex; + align-items: center; +} + +.switches { + display: flex; + flex-direction: row; + gap: 20px; + flex-wrap: wrap; + margin-top: 20px; +} diff --git a/src/screens/UserPortal/Events/Events.test.tsx b/src/screens/UserPortal/Events/Events.test.tsx new file mode 100644 index 0000000000..4243efa8ee --- /dev/null +++ b/src/screens/UserPortal/Events/Events.test.tsx @@ -0,0 +1,473 @@ +import React from 'react'; +import { act, render, screen } from '@testing-library/react'; +import { MockedProvider } from '@apollo/react-testing'; +import { I18nextProvider } from 'react-i18next'; + +import { ORGANIZATION_EVENTS_CONNECTION } from 'GraphQl/Queries/Queries'; +import { BrowserRouter } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { store } from 'state/store'; +import i18nForTest from 'utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import Events from './Events'; +import userEvent from '@testing-library/user-event'; +import * as getOrganizationId from 'utils/getOrganizationId'; +import { CREATE_EVENT_MUTATION } from 'GraphQl/Mutations/mutations'; +import { toast } from 'react-toastify'; +import dayjs from 'dayjs'; + +jest.mock('react-toastify', () => ({ + toast: { + error: jest.fn(), + info: jest.fn(), + success: jest.fn(), + }, +})); + +const MOCKS = [ + { + request: { + query: ORGANIZATION_EVENTS_CONNECTION, + variables: { + organization_id: '', + title_contains: '', + }, + }, + result: { + data: { + eventsByOrganizationConnection: [ + { + _id: '6404a267cc270739118e2349', + title: 'NewEvent', + description: 'sdadsasad', + startDate: '2023-03-05', + endDate: '2023-03-05', + location: 'NewLocation', + startTime: null, + endTime: null, + allDay: true, + recurring: false, + isPublic: true, + isRegisterable: false, + creator: { + _id: '63d649417ffe6e4d5174ea32', + firstName: 'Noble', + lastName: 'Mittal', + __typename: 'User', + }, + attendees: [ + { + _id: '63d649417ffe6e4d5174ea32', + __typename: 'User', + }, + { + _id: '63d6064458fce20ee25c3bf7', + __typename: 'User', + }, + ], + __typename: 'Event', + }, + { + _id: '6404e952c651df745358849d', + title: '1parti', + description: 'asddas', + startDate: '2023-03-06', + endDate: '2023-03-06', + location: 'das', + startTime: '00:40:00.000Z', + endTime: '02:40:00.000Z', + allDay: false, + recurring: false, + isPublic: true, + isRegisterable: true, + creator: { + _id: '63d649417ffe6e4d5174ea32', + firstName: 'Noble', + lastName: 'Mittal', + __typename: 'User', + }, + attendees: [ + { + _id: '63d649417ffe6e4d5174ea32', + __typename: 'User', + }, + { + _id: '63dd52bbe69f63814b0a5dd4', + __typename: 'User', + }, + { + _id: '63d6064458fce20ee25c3bf7', + __typename: 'User', + }, + ], + __typename: 'Event', + }, + ], + }, + }, + }, + { + request: { + query: ORGANIZATION_EVENTS_CONNECTION, + variables: { + organization_id: '', + title_contains: 'test', + }, + }, + result: { + data: { + eventsByOrganizationConnection: [ + { + _id: '6404a267cc270739118e2349', + title: 'NewEvent', + description: 'sdadsasad', + startDate: '2023-03-05', + endDate: '2023-03-05', + location: 'NewLocation', + startTime: null, + endTime: null, + allDay: true, + recurring: false, + isPublic: true, + isRegisterable: false, + creator: { + _id: '63d649417ffe6e4d5174ea32', + firstName: 'Noble', + lastName: 'Mittal', + __typename: 'User', + }, + attendees: [ + { + _id: '63d649417ffe6e4d5174ea32', + __typename: 'User', + }, + { + _id: '63d6064458fce20ee25c3bf7', + __typename: 'User', + }, + ], + __typename: 'Event', + }, + ], + }, + }, + }, + { + request: { + query: CREATE_EVENT_MUTATION, + variables: { + title: 'testEventTitle', + description: 'testEventDescription', + location: 'testEventLocation', + isPublic: true, + recurring: false, + isRegisterable: true, + organizationId: '', + startDate: dayjs(new Date()).format('YYYY-MM-DD'), + endDate: dayjs(new Date()).format('YYYY-MM-DD'), + allDay: false, + startTime: '08:00:00Z', + endTime: '10:00:00Z', + }, + }, + result: { + data: { + createEvent: { + _id: '2', + }, + }, + }, + }, + { + request: { + query: CREATE_EVENT_MUTATION, + variables: { + title: 'testEventTitle', + description: 'testEventDescription', + location: 'testEventLocation', + isPublic: true, + recurring: false, + isRegisterable: true, + organizationId: '', + startDate: dayjs(new Date()).format('YYYY-MM-DD'), + endDate: dayjs(new Date()).format('YYYY-MM-DD'), + allDay: true, + startTime: null, + endTime: null, + }, + }, + result: { + data: { + createEvent: { + _id: '1', + }, + }, + }, + }, +]; + +const link = new StaticMockLink(MOCKS, true); + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +describe('Testing Events Screen [User Portal]', () => { + jest.mock('utils/getOrganizationId'); + + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); + + test('Screen should be rendered properly', async () => { + const getOrganizationIdSpy = jest + .spyOn(getOrganizationId, 'default') + .mockImplementation(() => { + return ''; + }); + + render( + + + + + + + + + + ); + + await wait(); + + expect(getOrganizationIdSpy).toHaveBeenCalled(); + }); + + test('Events are visible as expected without search query', async () => { + const getOrganizationIdSpy = jest + .spyOn(getOrganizationId, 'default') + .mockImplementation(() => { + return ''; + }); + + render( + + + + + + + + + + ); + + await wait(); + + let mockEventTitle = ''; + if (MOCKS[0].result?.data.eventsByOrganizationConnection) { + mockEventTitle = + MOCKS[0].result?.data.eventsByOrganizationConnection[0].title; + } + + expect(getOrganizationIdSpy).toHaveBeenCalled(); + expect(screen.queryByText(mockEventTitle)).toBeInTheDocument(); + }); + + test('Search works as expected when user types in search input', async () => { + const getOrganizationIdSpy = jest + .spyOn(getOrganizationId, 'default') + .mockImplementation(() => { + return ''; + }); + + render( + + + + + + + + + + ); + + await wait(); + + expect(getOrganizationIdSpy).toHaveBeenCalled(); + + const randomSearchInput = 'test'; + userEvent.type(screen.getByTestId('searchInput'), randomSearchInput); + + await wait(); + + let mockEventTitle = ''; + if (MOCKS[0].result?.data.eventsByOrganizationConnection) { + mockEventTitle = + MOCKS[0].result?.data.eventsByOrganizationConnection[0].title; + } + + let mockEventTitleAbsent = ''; + if (MOCKS[0].result?.data.eventsByOrganizationConnection) { + mockEventTitleAbsent = + MOCKS[0].result?.data.eventsByOrganizationConnection[1].title; + } + + expect(screen.queryByText(mockEventTitle)).toBeInTheDocument(); + expect(screen.queryByText(mockEventTitleAbsent)).not.toBeInTheDocument(); + }); + + test('Create event works as expected when event is not an all day event.', async () => { + const getOrganizationIdSpy = jest + .spyOn(getOrganizationId, 'default') + .mockImplementation(() => { + return ''; + }); + + render( + + + + + + + + + + ); + + await wait(); + + expect(getOrganizationIdSpy).toHaveBeenCalled(); + userEvent.click(screen.getByTestId('createEventModalBtn')); + + const randomEventTitle = 'testEventTitle'; + const randomEventDescription = 'testEventDescription'; + const randomEventLocation = 'testEventLocation'; + + userEvent.type(screen.getByTestId('eventTitleInput'), randomEventTitle); + userEvent.type( + screen.getByTestId('eventDescriptionInput'), + randomEventDescription + ); + userEvent.type( + screen.getByTestId('eventLocationInput'), + randomEventLocation + ); + + userEvent.click(screen.getByTestId('publicEventCheck')); + userEvent.click(screen.getByTestId('publicEventCheck')); + + userEvent.click(screen.getByTestId('registerableEventCheck')); + userEvent.click(screen.getByTestId('registerableEventCheck')); + + userEvent.click(screen.getByTestId('recurringEventCheck')); + userEvent.click(screen.getByTestId('recurringEventCheck')); + + userEvent.click(screen.getByTestId('recurringEventCheck')); + userEvent.click(screen.getByTestId('recurringEventCheck')); + + userEvent.click(screen.getByTestId('allDayEventCheck')); + + userEvent.click(screen.getByTestId('createEventBtn')); + + await wait(); + + expect(toast.success).toBeCalledWith( + 'Event created and posted successfully.' + ); + }); + + test('Create event works as expected when event is an all day event.', async () => { + const getOrganizationIdSpy = jest + .spyOn(getOrganizationId, 'default') + .mockImplementation(() => { + return ''; + }); + + render( + + + + + + + + + + ); + + await wait(); + + expect(getOrganizationIdSpy).toHaveBeenCalled(); + userEvent.click(screen.getByTestId('createEventModalBtn')); + + const randomEventTitle = 'testEventTitle'; + const randomEventDescription = 'testEventDescription'; + const randomEventLocation = 'testEventLocation'; + + userEvent.type(screen.getByTestId('eventTitleInput'), randomEventTitle); + userEvent.type( + screen.getByTestId('eventDescriptionInput'), + randomEventDescription + ); + userEvent.type( + screen.getByTestId('eventLocationInput'), + randomEventLocation + ); + + userEvent.click(screen.getByTestId('createEventBtn')); + + await wait(); + + expect(toast.success).toBeCalledWith( + 'Event created and posted successfully.' + ); + }); + + test('Switch to calendar view works as expected.', async () => { + const getOrganizationIdSpy = jest + .spyOn(getOrganizationId, 'default') + .mockImplementation(() => { + return ''; + }); + + render( + + + + + + + + + + ); + + await wait(); + expect(getOrganizationIdSpy).toHaveBeenCalled(); + + userEvent.click(screen.getByTestId('modeChangeBtn')); + userEvent.click(screen.getByTestId('modeBtn1')); + + await wait(); + const calenderView = 'Calendar View'; + + expect(screen.queryAllByText(calenderView)).not.toBeNull(); + }); +}); diff --git a/src/screens/UserPortal/Events/Events.tsx b/src/screens/UserPortal/Events/Events.tsx new file mode 100644 index 0000000000..caf1d72fd2 --- /dev/null +++ b/src/screens/UserPortal/Events/Events.tsx @@ -0,0 +1,485 @@ +import React from 'react'; +import OrganizationNavbar from 'components/UserPortal/OrganizationNavbar/OrganizationNavbar'; +import OrganizationSidebar from 'components/UserPortal/OrganizationSidebar/OrganizationSidebar'; +import EventCard from 'components/UserPortal/EventCard/EventCard'; +import UserSidebar from 'components/UserPortal/UserSidebar/UserSidebar'; +import { Button, Dropdown, Form, InputGroup } from 'react-bootstrap'; +import PaginationList from 'components/PaginationList/PaginationList'; +import { ORGANIZATION_EVENTS_CONNECTION } from 'GraphQl/Queries/Queries'; +import { useMutation, useQuery } from '@apollo/client'; +import { SearchOutlined } from '@mui/icons-material'; +import styles from './Events.module.css'; +import { useTranslation } from 'react-i18next'; +import HourglassBottomIcon from '@mui/icons-material/HourglassBottom'; +import getOrganizationId from 'utils/getOrganizationId'; +import Modal from 'react-bootstrap/Modal'; +import ReactDatePicker from 'react-datepicker'; +import { CREATE_EVENT_MUTATION } from 'GraphQl/Mutations/mutations'; +import dayjs from 'dayjs'; +import { toast } from 'react-toastify'; +import { errorHandler } from 'utils/errorHandler'; + +interface InterfaceEventCardProps { + id: string; + title: string; + description: string; + location: string; + startDate: string; + endDate: string; + isRegisterable: boolean; + isPublic: boolean; + endTime: string; + startTime: string; + recurring: boolean; + allDay: boolean; + creator: { + firstName: string; + lastName: string; + id: string; + }; + registrants: { + id: string; + }[]; +} + +export default function events(): JSX.Element { + const { t } = useTranslation('translation', { + keyPrefix: 'userEvents', + }); + + const [page, setPage] = React.useState(0); + const [rowsPerPage, setRowsPerPage] = React.useState(5); + const [events, setEvents] = React.useState([]); + const [filterName, setFilterName] = React.useState(''); + const [mode, setMode] = React.useState(0); + const [showCreateEventModal, setShowCreateEventModal] = React.useState(false); + const [eventTitle, setEventTitle] = React.useState(''); + const [eventDescription, setEventDescription] = React.useState(''); + const [eventLocation, setEventLocation] = React.useState(''); + const [startDate, setStartDate] = React.useState(new Date()); + const [endDate, setEndDate] = React.useState(new Date()); + const [isPublic, setIsPublic] = React.useState(true); + const [isRegisterable, setIsRegisterable] = React.useState(true); + const [isRecurring, setIsRecurring] = React.useState(false); + const [isAllDay, setIsAllDay] = React.useState(true); + const [startTime, setStartTime] = React.useState('08:00:00'); + const [endTime, setEndTime] = React.useState('10:00:00'); + + const organizationId = getOrganizationId(window.location.href); + + const modes = [t('listView'), t('calendarView')]; + + const { data, loading, refetch } = useQuery(ORGANIZATION_EVENTS_CONNECTION, { + variables: { + organization_id: organizationId, + title_contains: '', + }, + }); + + const [create] = useMutation(CREATE_EVENT_MUTATION); + + const createEvent = async (): Promise => { + try { + const { data: createEventData } = await create({ + variables: { + title: eventTitle, + description: eventDescription, + isPublic, + recurring: isRecurring, + isRegisterable: isRegisterable, + organizationId, + startDate: dayjs(startDate).format('YYYY-MM-DD'), + endDate: dayjs(endDate).format('YYYY-MM-DD'), + allDay: isAllDay, + location: eventLocation, + startTime: !isAllDay ? startTime + 'Z' : null, + endTime: !isAllDay ? endTime + 'Z' : null, + }, + }); + + /* istanbul ignore next */ + if (createEventData) { + toast.success(t('eventCreated')); + refetch(); + setEventTitle(''); + setEventDescription(''); + setEventLocation(''); + setStartDate(new Date()); + setEndDate(new Date()); + setStartTime('08:00:00'); + setEndTime('10:00:00'); + } + } catch (error: any) { + /* istanbul ignore next */ + errorHandler(t, error); + } + }; + + /* istanbul ignore next */ + const handleChangePage = ( + _event: React.MouseEvent | null, + newPage: number + ): void => { + setPage(newPage); + }; + + const toggleCreateEventModal = (): void => + setShowCreateEventModal(!showCreateEventModal); + + /* istanbul ignore next */ + const handleChangeRowsPerPage = ( + event: React.ChangeEvent + ): void => { + const newRowsPerPage = event.target.value; + + setRowsPerPage(parseInt(newRowsPerPage, 10)); + setPage(0); + }; + + const handleSearch = ( + event: React.ChangeEvent + ): void => { + const newFilter = event.target.value; + setFilterName(newFilter); + const filter = { + title_contains: newFilter, + }; + setPage(0); + refetch(filter); + }; + + const handleEventTitleChange = ( + event: React.ChangeEvent + ): void => { + setEventTitle(event.target.value); + }; + + const handleEventLocationChange = ( + event: React.ChangeEvent + ): void => { + setEventLocation(event.target.value); + }; + + const handleEventDescriptionChange = ( + event: React.ChangeEvent + ): void => { + setEventDescription(event.target.value); + }; + + /* istanbul ignore next */ + const handleStartDateChange = (newDate: any): void => { + setStartDate(newDate); + }; + + /* istanbul ignore next */ + const handleEndDateChange = (newDate: any): void => { + setEndDate(newDate); + }; + + /* istanbul ignore next */ + React.useEffect(() => { + if (data) { + setEvents(data.eventsByOrganizationConnection); + } + }, [data]); + + const navbarProps = { + currentPage: 'events', + }; + + return ( + <> + +
+ +
+
+ + + + + + +
+ + + + {modes[mode]} + + + {modes.map((value, index) => { + return ( + setMode(index)} + > + {value} + + ); + })} + + +
+
+
+
+ {loading ? ( +
+ Loading... +
+ ) : ( + <> + {events && events.length > 0 ? ( + (rowsPerPage > 0 + ? events.slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage + ) + : /* istanbul ignore next */ + events + ).map((event: any) => { + const attendees: any = []; + event.attendees.forEach((attendee: any) => { + const r = { + id: attendee._id, + }; + + attendees.push(r); + }); + + const creator: any = {}; + creator.firstName = event.creator.firstName; + creator.lastName = event.creator.lastName; + creator.id = event.creator._id; + + const cardProps: InterfaceEventCardProps = { + id: event._id, + title: event.title, + description: event.description, + location: event.location, + startDate: event.startDate, + endDate: event.endDate, + isRegisterable: event.isRegisterable, + isPublic: event.isPublic, + endTime: event.endTime, + startTime: event.startTime, + recurring: event.recurring, + allDay: event.allDay, + registrants: attendees, + creator, + }; + + return ; + }) + ) : ( + {t('nothingToShow')} + )} + + )} +
+ + + + + + +
+
+
+ + + +
{t('createEvent')}
+ +
+ + + + {t('eventTitle')} + + + + + + + {t('eventDescription')} + + + + + + + {t('eventLocation')} + + + +
{t('startDate')}
+ +
{t('endDate')}
+ +
+
+ + setIsPublic(!isPublic)} + /> +
+ +
+ + setIsRegisterable(!isRegisterable)} + /> +
+ +
+ + setIsRecurring(!isRecurring)} + /> +
+ +
+ + setIsAllDay(!isAllDay)} + /> +
+ + {!isAllDay && ( +
+
+ + setStartTime(e.target.value) + } + /> +
+
+ + setEndTime(e.target.value) + } + /> +
+
+ )} +
+
+ + + + +
+
+ + ); +} diff --git a/src/screens/UserPortal/Home/Home.test.tsx b/src/screens/UserPortal/Home/Home.test.tsx index 809572073d..0c9809030a 100644 --- a/src/screens/UserPortal/Home/Home.test.tsx +++ b/src/screens/UserPortal/Home/Home.test.tsx @@ -49,6 +49,7 @@ const MOCKS = [ }, likeCount: 0, commentCount: 0, + comments: [], likedBy: [], }, { @@ -64,7 +65,41 @@ const MOCKS = [ email: 'adidacreator1@gmail.com', }, likeCount: 0, - commentCount: 0, + commentCount: 2, + comments: [ + { + _id: '64eb13beca85de60ebe0ed0e', + creator: { + _id: '63d6064458fce20ee25c3bf7', + firstName: 'Noble', + lastName: 'Mittal', + email: 'test@gmail.com', + __typename: 'User', + }, + likeCount: 1, + likedBy: [ + { + _id: 1, + }, + ], + text: 'First comment from Talawa user portal.', + __typename: 'Comment', + }, + { + _id: '64eb483aca85de60ebe0ef99', + creator: { + _id: '63d6064458fce20ee25c3bf7', + firstName: 'Noble', + lastName: 'Mittal', + email: 'test@gmail.com', + __typename: 'User', + }, + likeCount: 0, + likedBy: [], + text: 'Great View', + __typename: 'Comment', + }, + ], likedBy: [ { _id: '63d6064458fce20ee25c3bf7', diff --git a/src/screens/UserPortal/Home/Home.tsx b/src/screens/UserPortal/Home/Home.tsx index 29792c8a22..b657fc0c02 100644 --- a/src/screens/UserPortal/Home/Home.tsx +++ b/src/screens/UserPortal/Home/Home.tsx @@ -33,6 +33,19 @@ interface InterfacePostCardProps { title: string; likeCount: number; commentCount: number; + comments: { + creator: { + _id: string; + firstName: string; + lastName: string; + email: string; + }; + likeCount: number; + likedBy: { + id: string; + }[]; + text: string; + }[]; likedBy: { firstName: string; lastName: string; @@ -188,6 +201,33 @@ export default function home(): JSX.Element { allLikes.push(singleLike); }); + const postComments: any = []; + post.comments.forEach((value: any) => { + const commentLikes: any = []; + + value.likedBy.forEach((commentLike: any) => { + const singleLike = { + id: commentLike._id, + }; + commentLikes.push(singleLike); + }); + + const singleCommnet: any = { + id: value._id, + creator: { + firstName: value.creator.firstName, + lastName: value.creator.lastName, + id: value.creator._id, + email: value.creator.email, + }, + likeCount: value.likeCount, + likedBy: commentLikes, + text: value.text, + }; + + postComments.push(singleCommnet); + }); + const cardProps: InterfacePostCardProps = { id: post._id, creator: { @@ -202,6 +242,7 @@ export default function home(): JSX.Element { text: post.text, likeCount: post.likeCount, commentCount: post.commentCount, + comments: postComments, likedBy: allLikes, }; From 7b846e7eb59b9ec87abcc05f4cc6b73b84aa572b Mon Sep 17 00:00:00 2001 From: Peter Harrison <16875803+palisadoes@users.noreply.github.com> Date: Sat, 23 Sep 2023 19:30:41 -0700 Subject: [PATCH 4/4] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index aebc481244..39be8dbb3d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Talawa Admin 💬 Join the community on Slack. The link can be found in the `Talawa` [README.md](https://github.com/PalisadoesFoundation/talawa) file. +![talawa-logo-lite-200x200](https://github.com/PalisadoesFoundation/talawa-admin/assets/16875803/26291ec5-d3c1-4135-8bc7-80885dff613d) + [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![GitHub stars](https://img.shields.io/github/stars/PalisadoesFoundation/talawa-admin.svg?style=social&label=Star&maxAge=2592000)](https://github.com/PalisadoesFoundation/talawa-admin) [![GitHub forks](https://img.shields.io/github/forks/PalisadoesFoundation/talawa-admin.svg?style=social&label=Fork&maxAge=2592000)](https://github.com/PalisadoesFoundation/talawa-admin)