diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 240dc115f61..3968b850d7b 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -82,6 +82,7 @@ "updatedSuccessfully": "{{item}} updated Successfully", "removedSuccessfully": "{{item}} removed Successfully", "successfullyUpdated": "Successfully Updated", + "confirm": "Confirm", "sessionWarning": "Your session will expire soon due to inactivity. Please interact with the page to extend your session.", "sessionLogOut": "Your session has expired due to inactivity. Please log in again to continue.", "sort": "Sort" diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index cba39a3b721..d84646d7884 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -213,7 +213,8 @@ "roleUpdated": "Role Updated.", "joinNow": "Join Now", "visit": "Visit", - "withdraw": "Widthdraw", + "leave": "Leave", + "withdraw": "Withdraw", "removeUserFrom": "Remove User from {{org}}", "removeConfirmation": "Are you sure you want to remove '{{name}}' from organization '{{org}}'?", "searchByName": "searchByName", @@ -982,6 +983,7 @@ "logout": "Logout", "settings": "Settings", "chat": "Chat", + "leave": "Leave Organization", "menu": "menu" }, "organizationSidebar": { @@ -1205,7 +1207,8 @@ "deleteCategory": "Delete Category", "deleteCategoryMsg": "Are you sure you want to delete this Action Item Category?", "createButton": "createButton", - "editButton": "editButton" + "editButton": "editButton", + "categoryDetails": "categoryDetails" }, "organizationVenues": { "title": "Venues", @@ -1319,5 +1322,12 @@ }, "userPledges": { "title": "My Pledges" + }, + "orgLeave": { + "confirmation": "Are you sure you want to leave the organization? ", + "heading": "Leaving organization ?", + "description": "Are you sure you want to leave the organization? This action cannot be undone. If the organization requires approval, you will not be able to rejoin without it. Any related data or permissions may also be lost.", + "errorOccured": "An error occurred. Please try again later.", + "orgLeft": "You have left the organization." } } diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index cba04cea96d..6ffcc802b9b 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -83,6 +83,7 @@ "updatedSuccessfully": "{{item}} mis à jour avec succès", "removedSuccessfully": "{{item}} supprimé avec succès", "successfullyUpdated": "Mis à jour avec succès", + "confirm": "Confirmer", "sessionWarning": "Votre session expirera bientôt en raison de l'inactivité. Veuillez interagir avec la page pour prolonger votre session.", "sessionLogOut": "Votre session a expiré en raison de l'inactivité. Veuillez vous reconnecter pour continuer." } diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 525a3dba793..4ef408ba3f2 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -213,6 +213,7 @@ "roleUpdated": "Rôle mis à jour.", "joinNow": "Adhérer maintenant", "visit": "Visite", + "leave": "partir", "withdraw": "Largeur de tirage", "removeUserFrom": "Supprimer l'Utilisateur de {{org}}", "removeConfirmation": "Êtes-vous sûr de vouloir supprimer '{{name}}' de l'organisation '{{org}}' ?", @@ -977,6 +978,7 @@ "talawaUserPortal": "Portail utilisateur Talawa", "my organizations": "Mes organisations", "communityProfile": "Profil de la communauté", + "leave": "Quitter l'organisation", "users": "utilisateurs", "requests": "demandes", "logout": "se déconnecter", @@ -1205,7 +1207,8 @@ "deleteCategory": "Supprimer la catégorie", "deleteCategoryMsg": "Êtes-vous sûr de vouloir supprimer cette catégorie d'élément d'action ?", "createButton": "Créer", - "editButton": "Modifier" + "editButton": "Modifier", + "categoryDetails": "Détails de la catégorie" }, "organizationVenues": { "title": "Lieux", @@ -1319,5 +1322,12 @@ }, "userPledges": { "title": "Mes Promesses" + }, + "orgLeave": { + "confirmation": "Etes-vous sûr de vouloir quitter l'organisation ? ", + "heading": "Quitter l'organisation ?", + "description": "Etes-vous sûr de vouloir quitter l'organisation ? Cette action ne peut pas être annulée. Si l'organisation nécessite une approbation, vous ne pourrez pas la rejoindre sans cette approbation. Toutes les données ou autorisations associées peuvent également être perdues.", + "errorOccured": "Une erreur s'est produite. Veuillez réessayer plus tard.", + "orgLeft": "Vous avez quitté l'organisation." } } diff --git a/public/locales/hi/common.json b/public/locales/hi/common.json index d8843bc5001..07d89e5890e 100644 --- a/public/locales/hi/common.json +++ b/public/locales/hi/common.json @@ -83,6 +83,7 @@ "updatedSuccessfully": "{{item}} सफलतापूर्वक अपडेट किया गया", "removedSuccessfully": "{{item}} सफलतापूर्वक हटाया गया", "successfullyUpdated": "सफलतापूर्वक अपडेट किया गया", + "confirm": "पुष्टि करें", "sessionWarning": "आपका सत्र निष्क्रियता के कारण जल्द ही समाप्त हो जाएगा। कृपया अपने सत्र को बढ़ाने के लिए पृष्ठ के साथ बातचीत करें।", "sessionLogOut": "निष्क्रियता के कारण आपका सत्र समाप्त हो गया है। कृपया जारी रखने के लिए पुनः लॉगिन करें।" } diff --git a/public/locales/hi/translation.json b/public/locales/hi/translation.json index e827608c562..c54ca89c4e2 100644 --- a/public/locales/hi/translation.json +++ b/public/locales/hi/translation.json @@ -216,6 +216,7 @@ "withdraw": "वापस लें", "removeUserFrom": "{{org}} से उपयोगकर्ता को हटाएं", "removeConfirmation": "क्या आप वाकई '{{name}}' को संगठन '{{org}}' से हटाना चाहते हैं?", + "leave": "छोड़ो", "searchByName": "नाम से खोजें", "users": "उपयोगकर्ता", "name": "नाम", @@ -977,6 +978,7 @@ "talawaUserPortal": "तलावा उपयोगकर्ता पोर्टल", "my organizations": "मेरे संगठन", "communityProfile": "सामुदायिक प्रोफ़ाइल", + "leave": "संगठन छोड़ें", "users": "उपयोगकर्ता", "requests": "अनुरोध", "logout": "लॉग आउट", @@ -1205,7 +1207,8 @@ "deleteCategory": "श्रेणी हटाएं", "deleteCategoryMsg": "क्या आप वाकई इस कार्रवाई आइटम श्रेणी को हटाना चाहते हैं?", "createButton": "बटन बनाएं", - "editButton": "संपादित बटन" + "editButton": "संपादित बटन", + "categoryDetails": "श्रेणी विवरण" }, "organizationVenues": { "title": "स्थानों", @@ -1319,5 +1322,12 @@ }, "userPledges": { "title": "मेरी प्रतिज्ञाएँ" + }, + "orgLeave": { + "confirmation": "क्या आप वाकई संगठन छोड़ना चाहते हैं?", + "heading": "संगठन छोड़ रहे हैं?", + "description": "क्या आप वाकई संगठन छोड़ना चाहते हैं? इस एक्शन को वापस नहीं किया जा सकता। यदि संगठन को अनुमोदन की आवश्यकता है, तो आप इसके बिना पुनः शामिल नहीं हो पाएंगे। कोई भी संबंधित डेटा या अनुमतियाँ भी खो सकती हैं।", + "errorOccured": "एक त्रुटि पाई गई। कृपया बाद में पुन: प्रयास करें।", + "orgLeft": "आपने संगठन छोड़ दिया है." } } diff --git a/public/locales/sp/common.json b/public/locales/sp/common.json index 39ec00f1d1a..33af23d9f94 100644 --- a/public/locales/sp/common.json +++ b/public/locales/sp/common.json @@ -83,6 +83,7 @@ "updatedSuccessfully": "{{item}} actualizado con éxito", "removedSuccessfully": "{{item}} eliminado con éxito", "successfullyUpdated": "Actualizado con éxito", + "confirm": "Confirmar", "sessionWarning": "Su sesión expirará pronto debido a la inactividad. Por favor, interactúe con la página para extender su sesión.", "sessionLogOut": "Su sesión ha expirado debido a la inactividad. Por favor, inicie sesión nuevamente para continuar." } diff --git a/public/locales/sp/translation.json b/public/locales/sp/translation.json index f417edffff8..5b9ae133952 100644 --- a/public/locales/sp/translation.json +++ b/public/locales/sp/translation.json @@ -233,6 +233,7 @@ "errorOccured": "Se produjo un error. Por favor, inténtalo de nuevo más tarde.", "removeUserFrom": "Eliminar Usuario de {{org}}", "removeConfirmation": "¿Está seguro de que desea eliminar a '{{name}}' de la organización '{{org}}'?", + "leave": "Dejar", "noOrgError": "Error sin organización" }, "communityProfile": { @@ -760,13 +761,13 @@ }, "orgSettings": { "title": "Configuración Talawa", - "pageName": "Configuración", "general": "General", "actionItemCategories": "Categorías de elementos de acción", - "updateYourDetails": "Actualiza tus datos", - "updateYourPassword": "Actualice su contraseña", "updateOrganization": "Actualizar Organización", "seeRequest": "Ver Solicitud", + "pageName": "Configuración", + "updateYourDetails": "Actualiza tus datos", + "updateYourPassword": "Actualice su contraseña", "settings": "Ajustes", "noData": "Sin datos", "otherSettings": "Otras Configuraciones", @@ -983,6 +984,7 @@ "communityProfile": "Perfil de la comunidad", "logout": "Cerrar sesión", "settings": "Ajustes", + "leave": "Dejar la organización", "chat": "Chat" }, "organizationSidebar": { @@ -1090,8 +1092,8 @@ "donate": "Donar", "nothingToShow": "Nada que mostrar aquí.", "success": "Donación exitosa", - "invalidAmount": "Ingrese un valor numérico para el monto de la donación.", - "donationAmountDescription": "Ingrese el valor numérico del monto de la donación.", + "invalidAmount": "Por favor, ingrese un valor numérico para el monto de la donación.", + "donationAmountDescription": "Por favor, ingrese el valor numérico para el monto de la donación.", "donationOutOfRange": "El monto de la donación debe estar entre {{min}} y {{max}}." }, "userEvents": { @@ -1202,11 +1204,12 @@ "sameNameConflict": "Cambie el nombre para realizar una actualización", "categoryEnabled": "Categoría de elemento de acción habilitada", "categoryDisabled": "Categoría de elemento de acción deshabilitada", - "noActionItemCategories": "No hay categorías de elementos de acción", + "noActionItemCategories": "No hay Categorías del Ítem de Acción", "status": "Estado", - "categoryDeleted": "Categoría de elemento de acción eliminada", - "deleteCategory": "Eliminar categoría", - "deleteCategoryMsg": "¿Está seguro de que desea eliminar esta categoría de elemento de acción?" + "categoryDeleted": "Categoría del Ítem de Acción eliminada con éxito", + "deleteCategory": "Eliminar Categoría", + "deleteCategoryMsg": "¿Está seguro de que desea eliminar esta Categoría del Ítem de Acción?", + "categoryDetails": "Detalles de la categoría" }, "organizationVenues": { "title": "Lugares", @@ -1320,5 +1323,12 @@ }, "userPledges": { "title": "Mis Promesas" + }, + "orgLeave": { + "confirmation": "¿Estás segura de que quieres dejar la organización?", + "heading": "¿Dejar la organización?", + "description": "¿Estás seguro de que quieres dejar la organización? Esta acción no se puede deshacer. Si la organización requiere aprobación, no podrás volver a unirte sin ella. También se pueden perder todos los datos o permisos relacionados.", + "errorOccured": "Se produjo un error. Inténtelo de nuevo más tarde.", + "orgLeft": "Has abandonado la organización." } } diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index 1e0c619d395..32b761494d6 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -83,6 +83,7 @@ "updatedSuccessfully": "{{item}} 更新成功", "removedSuccessfully": "{{item}} 删除成功", "successfullyUpdated": "更新成功", + "confirm": "确认", "sessionWarning": "由于不活动,您的会话即将过期。请与页面互动以延长您的会话。", "sessionLogOut": "由于不活动,您的会话已过期。请重新登录以继续。" } diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index e672d44e4ef..55d316c7ab7 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -233,7 +233,8 @@ "orgJoined": "已加入组织", "MembershipRequestSent": "会员请求已发送", "AlreadyJoined": "已加入", - "errorOccured": "发生错误" + "errorOccured": "发生错误", + "leave": "离开" }, "communityProfile": { "title": "社区简介", @@ -407,7 +408,7 @@ "assignmentDate": "分配日期", "active": "积极的", "clearFilters": "清除过滤器", - "completed": "完全的", + "completed": "已完成", "completionDate": "完成日期", "createActionItem": "创建操作项", "deleteActionItem": "删除操作项", @@ -982,7 +983,8 @@ "logout": "登出", "settings": "设置", "chat": "聊天", - "menu": "菜单" + "menu": "菜单", + "leave": "离开" }, "organizationSidebar": { "viewAll": "查看全部", @@ -1205,7 +1207,8 @@ "deleteCategory": "删除类别", "deleteCategoryMsg": "您确定要删除此操作项目类别吗?", "createButton": "创建按钮", - "editButton": "编辑按钮" + "editButton": "编辑按钮", + "categoryDetails": "类别详情" }, "organizationVenues": { "title": "场地", @@ -1319,5 +1322,12 @@ }, "userPledges": { "title": "我的承诺" + }, + "orgLeave": { + "confirmation": "您确定要离开该组织吗?", + "heading": "离开组织?", + "description": "您确定要离开该组织吗?此操作无法撤销。如果该组织需要批准,您将无法在没有批准的情况下重新加入。任何相关的数据或权限可能也会丢失。", + "errorOccured": "发生错误。请稍后再试。", + "orgLeft": "您已离开该组织。" } } diff --git a/src/GraphQl/Mutations/OrganizationMutations.ts b/src/GraphQl/Mutations/OrganizationMutations.ts index b61d70607b6..7f5345658c3 100644 --- a/src/GraphQl/Mutations/OrganizationMutations.ts +++ b/src/GraphQl/Mutations/OrganizationMutations.ts @@ -283,6 +283,14 @@ export const JOIN_PUBLIC_ORGANIZATION = gql` } `; +export const LEAVE_ORGANIZATION = gql` + mutation ($organizationId: ID!) { + leaveOrganization(organizationId: $organizationId) { + _id + } + } +`; + export const CANCEL_MEMBERSHIP_REQUEST = gql` mutation ($membershipRequestId: ID!) { cancelMembershipRequest(membershipRequestId: $membershipRequestId) { diff --git a/src/GraphQl/Mutations/mutations.ts b/src/GraphQl/Mutations/mutations.ts index daeef2e3bc9..055701e7f7b 100644 --- a/src/GraphQl/Mutations/mutations.ts +++ b/src/GraphQl/Mutations/mutations.ts @@ -728,6 +728,7 @@ export { CREATE_DIRECT_CHAT, CREATE_SAMPLE_ORGANIZATION_MUTATION, JOIN_PUBLIC_ORGANIZATION, + LEAVE_ORGANIZATION, PLUGIN_SUBSCRIPTION, REMOVE_CUSTOM_FIELD, REMOVE_SAMPLE_ORGANIZATION_MUTATION, diff --git a/src/components/UserPortal/OrganizationCard/OrganizationCard.module.css b/src/components/UserPortal/OrganizationCard/OrganizationCard.module.css index 15634f7ad0e..3173536b8a2 100644 --- a/src/components/UserPortal/OrganizationCard/OrganizationCard.module.css +++ b/src/components/UserPortal/OrganizationCard/OrganizationCard.module.css @@ -58,6 +58,13 @@ width: 8rem; } +.leaveBtn { + display: flex; + justify-content: space-around; + width: 8rem; /* You can adjust the width based on your needs */ + margin-right: 9rem; +} + .orgName { text-overflow: ellipsis; white-space: nowrap; @@ -139,9 +146,17 @@ margin-left: auto; display: block; } + + .orgCard .btnContainer { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + .joinBtn, .joinedBtn, - .withdrawBtn { + .withdrawBtn, + .leaveBtn { display: flex; justify-content: space-around; width: 100%; diff --git a/src/components/UserPortal/OrganizationCard/OrganizationCard.test.tsx b/src/components/UserPortal/OrganizationCard/OrganizationCard.test.tsx index ba13fe346d7..56f7b215add 100644 --- a/src/components/UserPortal/OrganizationCard/OrganizationCard.test.tsx +++ b/src/components/UserPortal/OrganizationCard/OrganizationCard.test.tsx @@ -204,6 +204,52 @@ describe('Testing OrganizationCard Component [User Portal]', () => { await wait(); }); + test('should navigate to organization page and open leave confirmation modal', async () => { + const cardProps = { + ...props, + membershipRequestStatus: 'accepted', // Set status to accepted + }; + + render( + + + + + + + + + , + ); + + await wait(); + + // Assert that "Visit" and "Leave" buttons are present + expect(screen.getByTestId('manageBtn')).toBeInTheDocument(); + expect(screen.getByTestId('leaveBtn')).toBeInTheDocument(); + + // Click the "Visit" button and assert the navigate function is called + fireEvent.click(screen.getByTestId('manageBtn')); + expect(window.location.pathname).toBe(`/user/organization/${cardProps.id}`); // Simulate navigation + + // Click the "Leave" button and assert that the modal opens + fireEvent.click(screen.getByTestId('leaveBtn')); + await wait(); + + // Modal should be shown + expect(screen.getByText('Leaving organization ?')).toBeInTheDocument(); // Adjust this as per your modal content + + const closeButton = screen.getByText('Cancel'); // Assuming the modal has a "Close" button, adjust as per your modal content + fireEvent.click(closeButton); + + await wait(); + + // Assert that the modal is no longer in the document + expect( + screen.queryByText('Leaving organization ?'), + ).not.toBeInTheDocument(); + }); + test('Component should be rendered properly if organization Image is not undefined', async () => { props = { ...props, diff --git a/src/components/UserPortal/OrganizationCard/OrganizationCard.tsx b/src/components/UserPortal/OrganizationCard/OrganizationCard.tsx index e1c2c23bebe..50a4631ed2b 100644 --- a/src/components/UserPortal/OrganizationCard/OrganizationCard.tsx +++ b/src/components/UserPortal/OrganizationCard/OrganizationCard.tsx @@ -17,6 +17,7 @@ import { import useLocalStorage from 'utils/useLocalstorage'; import Avatar from 'components/Avatar/Avatar'; import { useNavigate } from 'react-router-dom'; +import LeaveConfirmModal from '../UserSidebarOrg/LeaveConfirmModal'; const { getItem } = useLocalStorage(); @@ -77,6 +78,7 @@ function organizationCard(props: InterfaceOrganizationCardProps): JSX.Element { const { t: tCommon } = useTranslation('common'); const navigate = useNavigate(); + const [modalShow, setModalShow] = React.useState(false); // Mutations for handling organization memberships const [sendMembershipRequest] = useMutation(SEND_MEMBERSHIP_REQUEST, { @@ -188,16 +190,33 @@ function organizationCard(props: InterfaceOrganizationCardProps): JSX.Element { {props.membershipRequestStatus === 'accepted' && ( - +
+ + + setModalShow(false)} + /> +
)} {props.membershipRequestStatus === 'pending' && ( diff --git a/src/components/UserPortal/UserSidebarOrg/LeaveConfirmModal.module.css b/src/components/UserPortal/UserSidebarOrg/LeaveConfirmModal.module.css new file mode 100644 index 00000000000..9e6fad2efd0 --- /dev/null +++ b/src/components/UserPortal/UserSidebarOrg/LeaveConfirmModal.module.css @@ -0,0 +1,4 @@ +.leaveConfirmModalFooter { + display: flex; + justify-content: space-between; +} diff --git a/src/components/UserPortal/UserSidebarOrg/LeaveConfirmModal.test.tsx b/src/components/UserPortal/UserSidebarOrg/LeaveConfirmModal.test.tsx new file mode 100644 index 00000000000..7fc0037b7a3 --- /dev/null +++ b/src/components/UserPortal/UserSidebarOrg/LeaveConfirmModal.test.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import { + render, + screen, + fireEvent, + waitFor, + RenderResult, + act, +} from '@testing-library/react'; +import LeaveConfirmModal from './LeaveConfirmModal'; +import { MockedProvider } from '@apollo/client/testing'; +import { LEAVE_ORGANIZATION } from 'GraphQl/Mutations/OrganizationMutations'; +import { toast } from 'react-toastify'; +import i18n from 'utils/i18nForTest'; +import { I18nextProvider } from 'react-i18next'; +import { BrowserRouter } from 'react-router-dom'; + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +const mockNavigate = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + +const translations = { + ...JSON.parse( + JSON.stringify(i18n.getDataByLanguage('en')?.translation.orgLeave ?? {}), + ), + ...JSON.parse(JSON.stringify(i18n.getDataByLanguage('en')?.common ?? {})), + ...JSON.parse(JSON.stringify(i18n.getDataByLanguage('en')?.errors ?? {})), +}; + +const renderLeaveConfirmModal = ( + onHide: () => void, + mocks: any, +): RenderResult => { + return render( + + + + + + + , + ); +}; + +const mocks = [ + { + request: { + query: LEAVE_ORGANIZATION, + variables: { organizationId: '6437904485008f171cf29924' }, + }, + result: { + data: { + leaveOrganization: true, + }, + }, + }, +]; + +describe('LeaveConfirmModal', () => { + const onHide = jest.fn(); + + it('calls onHide when cancel button is clicked', () => { + renderLeaveConfirmModal(onHide, mocks); + + const cancelButton = screen.getByText(translations.cancel); + fireEvent.click(cancelButton); + expect(onHide).toHaveBeenCalled(); + }); + + it('calls leaveOrganization mutation and handles success', async () => { + const { debug } = renderLeaveConfirmModal(onHide, mocks); + + const confirmButton = screen.getByTestId('leave-confirm-modal-confirm-btn'); + + fireEvent.click(confirmButton); + + await wait(); + + await waitFor(() => { + debug(); + expect(onHide).toHaveBeenCalled(); + }); + }); + + it('handles error when leaveOrganization mutation fails', async () => { + const errorMocks = [ + { + request: { + query: LEAVE_ORGANIZATION, + variables: { organizationId: '6437904485008f171cf29924' }, + }, + error: new Error(translations.errorOccured), + }, + ]; + + renderLeaveConfirmModal(onHide, errorMocks); + + fireEvent.click(screen.getByText(translations.confirm)); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith(translations.errorOccured); + expect(onHide).toHaveBeenCalled(); + }); + }); + + it('LEAVE_ORGANIZATION mock has correct query', () => { + expect(mocks[0].request.query).toBe(LEAVE_ORGANIZATION); + }); + + it('LEAVE_ORGANIZATION mock has correct variables', () => { + expect(mocks[0].request.variables).toEqual({ + organizationId: '6437904485008f171cf29924', + }); + }); + + it('LEAVE_ORGANIZATION mock has correct result', () => { + expect(mocks[0].result).toEqual({ data: { leaveOrganization: true } }); + }); +}); diff --git a/src/components/UserPortal/UserSidebarOrg/LeaveConfirmModal.tsx b/src/components/UserPortal/UserSidebarOrg/LeaveConfirmModal.tsx new file mode 100644 index 00000000000..9c71525a9c8 --- /dev/null +++ b/src/components/UserPortal/UserSidebarOrg/LeaveConfirmModal.tsx @@ -0,0 +1,94 @@ +import type { FC } from 'react'; +import React from 'react'; +import styles from './LeaveConfirmModal.module.css'; +import { Button, Modal } from 'react-bootstrap'; +import { LEAVE_ORGANIZATION } from 'GraphQl/Mutations/OrganizationMutations'; +import { USER_ORGANIZATION_CONNECTION } from 'GraphQl/Queries/OrganizationQueries'; +import { useMutation } from '@apollo/client'; +import { toast } from 'react-toastify'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; + +interface InterfaceLeaveConfirmModalProps { + show: boolean; + onHide: () => void; + orgId: string; +} + +/** + * LeaveConfirmModal component for user to leave an organization. + * + * Provides: + * - Modal to confirm leaving an organization. + * + * @param orgId - ID of the current organization. + * @param show - Boolean indicating if the modal should be shown. + * @param onHide - Function to hide the modal. + * + * @returns JSX.Element - The rendered LeaveConfirmModal component. + */ + +const LeaveConfirmModal: FC = ({ + onHide, + show, + orgId, +}) => { + const { t: tCommon } = useTranslation('common'); + const { t } = useTranslation('translation', { + keyPrefix: 'orgLeave', + }); + const navigate = useNavigate(); + + const [leaveOrganization] = useMutation(LEAVE_ORGANIZATION, { + refetchQueries: [ + { query: USER_ORGANIZATION_CONNECTION, variables: { id: orgId } }, + ], + }); + + async function leaveOrg(): Promise { + try { + await leaveOrganization({ + variables: { + organizationId: orgId, + }, + }); + onHide(); + navigate('/user/organizations'); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(t('errorOccured')); + } + onHide(); + } + } + return ( + + + {t('heading')} + + +

{t('confirmation')}

+

{t('description')}

+
+ + + + +
+ ); +}; + +export default LeaveConfirmModal; diff --git a/src/components/UserPortal/UserSidebarOrg/UserSidebarOrg.module.css b/src/components/UserPortal/UserSidebarOrg/UserSidebarOrg.module.css index b300eb7e89c..34dc221a680 100644 --- a/src/components/UserPortal/UserSidebarOrg/UserSidebarOrg.module.css +++ b/src/components/UserPortal/UserSidebarOrg/UserSidebarOrg.module.css @@ -109,6 +109,31 @@ background-color: var(--bs-white); } +.leftDrawer .leaveOrgContainer { + display: flex; + align-items: center; + height: 30px; + background-color: white; + width: 100%; + border-radius: 16px; + border: none; +} + +.leftDrawer .leaveOrgContainer:focus { + outline: none; + border: none; +} + +.leftDrawer .leaveOrgContainer .primaryText { + color: red; +} + +.leftDrawer .leaveOrgContainer .leaveIcon { + transform: scale(1.5); + color: var(--bs-danger); + margin-right: 20px; +} + .leftDrawer .profileContainer:focus { outline: none; } diff --git a/src/components/UserPortal/UserSidebarOrg/UserSidebarOrg.test.tsx b/src/components/UserPortal/UserSidebarOrg/UserSidebarOrg.test.tsx index 2f28d9afd15..457304957c3 100644 --- a/src/components/UserPortal/UserSidebarOrg/UserSidebarOrg.test.tsx +++ b/src/components/UserPortal/UserSidebarOrg/UserSidebarOrg.test.tsx @@ -275,6 +275,37 @@ describe('Testing LeftDrawerOrg component for SUPERADMIN', () => { }); }); + test('Testing Leave Organization button functionality', async () => { + setItem('UserImage', ''); + setItem('SuperAdmin', true); + setItem('FirstName', 'John'); + setItem('LastName', 'Doe'); + + render( + + + + + + + + + , + ); + + await wait(); + + // Ensure the Leave button is rendered + const leaveButton = screen.getByTestId('leaveOrgButton'); + expect(leaveButton).toBeInTheDocument(); + + // Simulate clicking the Leave button + userEvent.click(leaveButton); + + // Check if the modal appears + expect(screen.getByText('Leaving organization ?')).toBeInTheDocument(); + }); + test('Testing Profile Page & Organization Detail Modal', async () => { setItem('UserImage', ''); setItem('SuperAdmin', true); @@ -415,4 +446,45 @@ describe('Testing LeftDrawerOrg component for SUPERADMIN', () => { , ); }); + + test('LeaveConfirmModal should open and close correctly', async () => { + setItem('UserImage', ''); + setItem('SuperAdmin', true); + setItem('FirstName', 'John'); + setItem('LastName', 'Doe'); + + render( + + + + + + + + + , + ); + + await wait(); + + // Ensure the Leave button is rendered + const leaveButton = screen.getByTestId('leaveOrgButton'); + expect(leaveButton).toBeInTheDocument(); + + // Simulate clicking the Leave button + userEvent.click(leaveButton); + + // Check if the modal appears + expect(screen.getByText('Leaving organization ?')).toBeInTheDocument(); + + // Find and click the close button + const closeButton = screen.getByText('Cancel'); + userEvent.click(closeButton); + + // Check if the modal is closed (no longer in the document) + await wait(); + expect( + screen.queryByText('Leaving organization ?'), + ).not.toBeInTheDocument(); + }); }); diff --git a/src/components/UserPortal/UserSidebarOrg/UserSidebarOrg.tsx b/src/components/UserPortal/UserSidebarOrg/UserSidebarOrg.tsx index 19036b23076..9814cf98ecd 100644 --- a/src/components/UserPortal/UserSidebarOrg/UserSidebarOrg.tsx +++ b/src/components/UserPortal/UserSidebarOrg/UserSidebarOrg.tsx @@ -13,6 +13,8 @@ import AngleRightIcon from 'assets/svgs/angleRight.svg?react'; import TalawaLogo from 'assets/svgs/talawa.svg?react'; import styles from './UserSidebarOrg.module.css'; import Avatar from 'components/Avatar/Avatar'; +import LogoutIcon from '@mui/icons-material/Logout'; +import LeaveConfirmModal from './LeaveConfirmModal'; export interface InterfaceUserSidebarOrgProps { orgId: string; @@ -45,6 +47,7 @@ const UserSidebarOrg = ({ // Translation hook for internationalization const { t } = useTranslation('translation', { keyPrefix: 'userSidebarOrg' }); const { t: tCommon } = useTranslation('common'); + const [modalShow, setModalShow] = React.useState(false); // State for managing dropdown visibility const [showDropdown, setShowDropdown] = React.useState(false); @@ -127,25 +130,45 @@ const UserSidebarOrg = ({ ) : ( - + <> + + + + setModalShow(false)} + /> + )}