From 190e5e349196abfc39f13bb45992f397e8c8cfaa Mon Sep 17 00:00:00 2001 From: Glen Dsouza Date: Sat, 7 Sep 2024 23:07:15 +0530 Subject: [PATCH] Redesign Action Items & ActionCategories, Shift AgendaCategory to Settings & Fix LeftDrawer Scroll (#2231) * Add scroll & fix org Avatar getting stretched in left drawer * Refac & restruct OrgSettings * Refac ActionItem Category & Add support for delete, Search, Sort & Filter * Refac & Redesign Global Action Items * Moved AgendaCategories to Settings & removed Delete for action categories * Redesigned Event Dashboard Parent container * Add new Event Action Screen & Delete old screens * Tests for OrgSettings, IconComponent, LeftDrawerOrg, routesReducer * Add test for Action Item Category * Add tests for OrganizationActionItems & related Modals * item modal test updation * Add tsdoc comments * Add CodeRabbit Suggestion for code Refac * fix tests --- public/locales/en/common.json | 11 +- public/locales/en/translation.json | 24 +- public/locales/fr/common.json | 11 +- public/locales/fr/translation.json | 26 +- public/locales/hi/common.json | 11 +- public/locales/hi/translation.json | 30 +- public/locales/sp/common.json | 11 +- public/locales/sp/translation.json | 36 +- public/locales/zh/common.json | 11 +- public/locales/zh/translation.json | 28 +- src/App.tsx | 6 - .../Mutations/ActionItemCategoryMutations.ts | 13 +- src/GraphQl/Mutations/ActionItemMutations.ts | 5 + .../Queries/ActionItemCategoryQueries.ts | 18 +- src/GraphQl/Queries/ActionItemQueries.ts | 16 +- src/assets/svgs/angleLeft.svg | 5 - src/assets/svgs/eventDashboard.svg | 7 - src/assets/svgs/eventStats.svg | 12 - .../ActionItemsContainer.module.css | 25 - .../ActionItems/ActionItemsContainer.test.tsx | 777 ----------- .../ActionItems/ActionItemsContainer.tsx | 568 -------- .../ActionItems/ActionItemsContainerMocks.ts | 112 -- .../ActionItems/ActionItemsContainerProps.ts | 131 -- .../ActionItems/ActionItemsModal.test.tsx | 294 ---- .../ActionItems/ActionItemsModal.tsx | 65 - .../ActionItems/ActionItemsModalBody.tsx | 257 ---- .../ActionItems/ActionItemsWrapper.module.css | 53 - .../ActionItems/ActionItemsWrapper.test.tsx | 73 - .../ActionItems/ActionItemsWrapper.tsx | 70 - .../AgendaCategoryContainer.tsx | 6 +- .../EditCustomFieldDropDown.test.tsx | 2 +- .../EditCustomFieldDropDown.tsx | 2 +- .../EventActionItems.module.css | 206 --- .../EventActionItems.test.tsx | 1178 ----------------- .../EventActionItems/EventActionItems.tsx | 599 --------- .../useEventActionColumnConfig.tsx | 201 --- .../IconComponent/IconComponent.test.tsx | 8 - .../IconComponent/IconComponent.tsx | 16 - .../LeftDrawerOrg/LeftDrawerOrg.module.css | 20 +- .../LeftDrawerOrg/LeftDrawerOrg.test.tsx | 2 +- .../LeftDrawerOrg/LeftDrawerOrg.tsx | 15 +- .../OrgActionItemCategories.module.css | 33 - .../OrgActionItemCategories.test.tsx | 372 ------ .../OrgActionItemCategories.tsx | 310 ----- .../OrgActionItemCategoryMocks.ts | 174 --- .../CategoryModal.test.tsx | 208 +++ .../ActionItemCategories/CategoryModal.tsx | 208 +++ .../OrgActionItemCategories.module.css | 138 ++ .../OrgActionItemCategories.test.tsx | 241 ++++ .../OrgActionItemCategories.tsx | 418 ++++++ .../OrgActionItemCategoryMocks.ts | 288 ++++ .../AgendaCategoryCreateModal.test.tsx | 0 .../AgendaCategoryCreateModal.tsx | 0 .../AgendaCategoryDeleteModal.tsx | 0 .../AgendaCategoryPreviewModal.tsx | 0 .../AgendaCategoryUpdateModal.test.tsx | 0 .../AgendaCategoryUpdateModal.tsx | 0 .../OrganizationAgendaCategory.module.css | 0 .../OrganizationAgendaCategory.test.tsx | 8 +- .../OrganizationAgendaCategory.tsx | 23 +- .../OrganizationAgendaCategoryErrorMocks.ts | 0 .../OrganizationAgendaCategoryMocks.ts | 0 .../General}/DeleteOrg/DeleteOrg.module.css | 0 .../General}/DeleteOrg/DeleteOrg.test.tsx | 0 .../General}/DeleteOrg/DeleteOrg.tsx | 0 .../OrgSettings/General/GeneralSettings.tsx | 73 + .../OrgProfileFieldSettings.module.css | 0 .../OrgProfileFieldSettings.test.tsx | 0 .../OrgProfileFieldSettings.tsx | 9 +- .../General}/OrgUpdate/OrgUpdate.module.css | 0 .../General}/OrgUpdate/OrgUpdate.test.tsx | 0 .../General}/OrgUpdate/OrgUpdate.tsx | 0 .../General}/OrgUpdate/OrgUpdateMocks.ts | 0 .../EventManagement.module.css | 8 - .../EventManagement/EventManagement.tsx | 185 +-- .../ForgotPassword/ForgotPassword.test.tsx | 47 +- src/screens/ForgotPassword/ForgotPassword.tsx | 22 +- .../FundCampaignPledge.test.tsx | 31 +- .../FundCampaignPledge/PledgesMocks.ts | 192 +-- src/screens/OrgSettings/OrgSettings.mocks.ts | 143 ++ .../OrgSettings/OrgSettings.module.css | 3 + src/screens/OrgSettings/OrgSettings.test.tsx | 207 +-- src/screens/OrgSettings/OrgSettings.tsx | 218 ++- .../ActionItemCreateModal.tsx | 167 --- .../ActionItemDeleteModal.tsx | 70 - .../ActionItemPreviewModal.tsx | 143 -- .../ActionItemUpdateModal.test.tsx | 235 ---- .../ActionItemUpdateModal.tsx | 172 --- .../ItemDeleteModal.test.tsx | 120 ++ .../ItemDeleteModal.tsx | 92 ++ .../ItemModal.test.tsx | 373 ++++++ .../OrganizationActionItems/ItemModal.tsx | 440 ++++++ .../ItemUpdateStatusModal.test.tsx | 173 +++ .../ItemUpdateStatusModal.tsx | 129 ++ .../ItemViewModal.test.tsx | 147 ++ .../OrganizationActionItems/ItemViewModal.tsx | 233 ++++ .../OrganizationActionItem.mocks.ts | 482 +++++++ .../OrganizationActionItemMocks.ts | 417 ------ .../OrganizationActionItems.module.css | 175 ++- .../OrganizationActionItems.test.tsx | 708 ++++------ .../OrganizationActionItems.tsx | 822 +++++++----- .../OrganizationActionItemsErrorMocks.ts | 266 ---- .../UserPortal/Campaigns/Campaigns.test.tsx | 31 +- .../UserPortal/Campaigns/CampaignsMocks.ts | 172 +-- .../UserPortal/Pledges/Pledge.test.tsx | 28 +- .../UserPortal/Pledges/PledgesMocks.ts | 122 +- src/state/reducers/routesReducer.test.ts | 18 - src/state/reducers/routesReducer.ts | 5 - src/utils/interfaces.ts | 14 +- 109 files changed, 5545 insertions(+), 8729 deletions(-) delete mode 100644 src/assets/svgs/angleLeft.svg delete mode 100644 src/assets/svgs/eventDashboard.svg delete mode 100644 src/assets/svgs/eventStats.svg delete mode 100644 src/components/ActionItems/ActionItemsContainer.module.css delete mode 100644 src/components/ActionItems/ActionItemsContainer.test.tsx delete mode 100644 src/components/ActionItems/ActionItemsContainer.tsx delete mode 100644 src/components/ActionItems/ActionItemsContainerMocks.ts delete mode 100644 src/components/ActionItems/ActionItemsContainerProps.ts delete mode 100644 src/components/ActionItems/ActionItemsModal.test.tsx delete mode 100644 src/components/ActionItems/ActionItemsModal.tsx delete mode 100644 src/components/ActionItems/ActionItemsModalBody.tsx delete mode 100644 src/components/ActionItems/ActionItemsWrapper.module.css delete mode 100644 src/components/ActionItems/ActionItemsWrapper.test.tsx delete mode 100644 src/components/ActionItems/ActionItemsWrapper.tsx delete mode 100644 src/components/EventManagement/EventActionItems/EventActionItems.module.css delete mode 100644 src/components/EventManagement/EventActionItems/EventActionItems.test.tsx delete mode 100644 src/components/EventManagement/EventActionItems/EventActionItems.tsx delete mode 100644 src/components/EventManagement/EventActionItems/useEventActionColumnConfig.tsx delete mode 100644 src/components/OrgActionItemCategories/OrgActionItemCategories.module.css delete mode 100644 src/components/OrgActionItemCategories/OrgActionItemCategories.test.tsx delete mode 100644 src/components/OrgActionItemCategories/OrgActionItemCategories.tsx delete mode 100644 src/components/OrgActionItemCategories/OrgActionItemCategoryMocks.ts create mode 100644 src/components/OrgSettings/ActionItemCategories/CategoryModal.test.tsx create mode 100644 src/components/OrgSettings/ActionItemCategories/CategoryModal.tsx create mode 100644 src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.module.css create mode 100644 src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.test.tsx create mode 100644 src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.tsx create mode 100644 src/components/OrgSettings/ActionItemCategories/OrgActionItemCategoryMocks.ts rename src/{screens/OrganizationAgendaCategory => components/OrgSettings/AgendaItemCategories}/AgendaCategoryCreateModal.test.tsx (100%) rename src/{screens/OrganizationAgendaCategory => components/OrgSettings/AgendaItemCategories}/AgendaCategoryCreateModal.tsx (100%) rename src/{screens/OrganizationAgendaCategory => components/OrgSettings/AgendaItemCategories}/AgendaCategoryDeleteModal.tsx (100%) rename src/{screens/OrganizationAgendaCategory => components/OrgSettings/AgendaItemCategories}/AgendaCategoryPreviewModal.tsx (100%) rename src/{screens/OrganizationAgendaCategory => components/OrgSettings/AgendaItemCategories}/AgendaCategoryUpdateModal.test.tsx (100%) rename src/{screens/OrganizationAgendaCategory => components/OrgSettings/AgendaItemCategories}/AgendaCategoryUpdateModal.tsx (100%) rename src/{screens/OrganizationAgendaCategory => components/OrgSettings/AgendaItemCategories}/OrganizationAgendaCategory.module.css (100%) rename src/{screens/OrganizationAgendaCategory => components/OrgSettings/AgendaItemCategories}/OrganizationAgendaCategory.test.tsx (96%) rename src/{screens/OrganizationAgendaCategory => components/OrgSettings/AgendaItemCategories}/OrganizationAgendaCategory.tsx (93%) rename src/{screens/OrganizationAgendaCategory => components/OrgSettings/AgendaItemCategories}/OrganizationAgendaCategoryErrorMocks.ts (100%) rename src/{screens/OrganizationAgendaCategory => components/OrgSettings/AgendaItemCategories}/OrganizationAgendaCategoryMocks.ts (100%) rename src/components/{ => OrgSettings/General}/DeleteOrg/DeleteOrg.module.css (100%) rename src/components/{ => OrgSettings/General}/DeleteOrg/DeleteOrg.test.tsx (100%) rename src/components/{ => OrgSettings/General}/DeleteOrg/DeleteOrg.tsx (100%) create mode 100644 src/components/OrgSettings/General/GeneralSettings.tsx rename src/components/{ => OrgSettings/General}/OrgProfileFieldSettings/OrgProfileFieldSettings.module.css (100%) rename src/components/{ => OrgSettings/General}/OrgProfileFieldSettings/OrgProfileFieldSettings.test.tsx (100%) rename src/components/{ => OrgSettings/General}/OrgProfileFieldSettings/OrgProfileFieldSettings.tsx (97%) rename src/components/{ => OrgSettings/General}/OrgUpdate/OrgUpdate.module.css (100%) rename src/components/{ => OrgSettings/General}/OrgUpdate/OrgUpdate.test.tsx (100%) rename src/components/{ => OrgSettings/General}/OrgUpdate/OrgUpdate.tsx (100%) rename src/components/{ => OrgSettings/General}/OrgUpdate/OrgUpdateMocks.ts (100%) delete mode 100644 src/screens/EventManagement/EventManagement.module.css create mode 100644 src/screens/OrgSettings/OrgSettings.mocks.ts delete mode 100644 src/screens/OrganizationActionItems/ActionItemCreateModal.tsx delete mode 100644 src/screens/OrganizationActionItems/ActionItemDeleteModal.tsx delete mode 100644 src/screens/OrganizationActionItems/ActionItemPreviewModal.tsx delete mode 100644 src/screens/OrganizationActionItems/ActionItemUpdateModal.test.tsx delete mode 100644 src/screens/OrganizationActionItems/ActionItemUpdateModal.tsx create mode 100644 src/screens/OrganizationActionItems/ItemDeleteModal.test.tsx create mode 100644 src/screens/OrganizationActionItems/ItemDeleteModal.tsx create mode 100644 src/screens/OrganizationActionItems/ItemModal.test.tsx create mode 100644 src/screens/OrganizationActionItems/ItemModal.tsx create mode 100644 src/screens/OrganizationActionItems/ItemUpdateStatusModal.test.tsx create mode 100644 src/screens/OrganizationActionItems/ItemUpdateStatusModal.tsx create mode 100644 src/screens/OrganizationActionItems/ItemViewModal.test.tsx create mode 100644 src/screens/OrganizationActionItems/ItemViewModal.tsx create mode 100644 src/screens/OrganizationActionItems/OrganizationActionItem.mocks.ts delete mode 100644 src/screens/OrganizationActionItems/OrganizationActionItemMocks.ts delete mode 100644 src/screens/OrganizationActionItems/OrganizationActionItemsErrorMocks.ts diff --git a/public/locales/en/common.json b/public/locales/en/common.json index f938663e5f..3cf8df2010 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -82,5 +82,14 @@ "updatedSuccessfully": "{{item}} updated Successfully", "removedSuccessfully": "{{item}} removed Successfully", "successfullyUpdated": "Successfully Updated", - "sort": "Sort" + "sort": "Sort", + "all": "All", + "active": "Active", + "disabled": "Disabled", + "pending": "Pending", + "completed": "Completed", + "late": "Late", + "createdLatest": "Created Latest", + "createdEarliest": "Created Earliest", + "searchBy": "Search by {{item}}" } diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 112eae5892..c5dac73839 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -283,9 +283,8 @@ "assignmentDate": "Assignment Date", "active": "Active", "clearFilters": "Clear Filters", - "completed": "Completed", "completionDate": "Completion Date", - "createActionItem": "Create Action Items", + "createActionItem": "Create Action Item", "deleteActionItem": "Delete Action Item", "deleteActionItemMsg": "Do you want to remove this action item?", "details": "Details", @@ -308,7 +307,14 @@ "successfulCreation": "Action Item created successfully", "successfulUpdation": "Action Item updated successfully", "successfulDeletion": "Action Item deleted successfully", - "title": "Action Items" + "title": "Action Items", + "category": "Category", + "allotedHours": "Alloted Hours", + "latestDueDate": "Latest Due Date", + "earliestDueDate": "Earliest Due Date", + "updateActionItem": "Update Action Item", + "noneUpdated": "None of the fields were updated", + "updateStatusMsg": "Are you sure you want to mark this action item as pending?" }, "organizationAgendaCategory": { "agendaCategoryDetails": "Agenda Category Details", @@ -601,7 +607,8 @@ "noData": "No data", "otherSettings": "Other Settings", "changeLanguage": "Change Language", - "manageCustomFields": "Manage Custom Fields" + "manageCustomFields": "Manage Custom Fields", + "agendaItemCategories": "Agenda Item Categories" }, "deleteOrg": { "deleteOrganization": "Delete Organization", @@ -937,13 +944,18 @@ "disableButton": "Disable", "updateActionItemCategory": "Update", "actionItemCategoryName": "Name", - "actionItemCategoryDetails": "Action Item Category Details", + "categoryDetails": "Category Details", "enterName": "Enter Name", "successfulCreation": "Action Item Category created successfully", "successfulUpdation": "Action Item Category updated successfully", "sameNameConflict": "Please change the name to make an update", "categoryEnabled": "Action Item Category Enabled", - "categoryDisabled": "Action Item Category Disabled" + "categoryDisabled": "Action Item Category Disabled", + "noActionItemCategories": "No Action Item Categories", + "status": "Status", + "categoryDeleted": "Action Item Category deleted successfully", + "deleteCategory": "Delete Category", + "deleteCategoryMsg": "Are you sure you want to delete this Action Item Category?" }, "organizationVenues": { "title": "Venues", diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index 250584da70..6796237d38 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -82,5 +82,14 @@ "addedSuccessfully": "{{item}} ajouté avec succès", "updatedSuccessfully": "{{item}} mis à jour avec succès", "removedSuccessfully": "{{item}} supprimé avec succès", - "successfullyUpdated": "Mis à jour avec succès" + "successfullyUpdated": "Mis à jour avec succès", + "all": "Tous", + "active": "Actif", + "disabled": "Désactivé", + "pending": "En attente", + "completed": "Complété", + "late": "En retard", + "createdLatest": "Créé le plus récemment", + "createdEarliest": "Créé le plus tôt", + "searchBy": "Rechercher par {{item}}" } diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 1498607e51..beb5c0c11c 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -286,7 +286,6 @@ "assignmentDate": "Date d'affectation", "active": "Actif", "clearFilters": "Effacer les filtres", - "completed": "Complété", "completionDate": "Date d'achèvement", "createActionItem": "Créer un élément d'action", "deleteActionItem": "Supprimer l'élément d'action", @@ -311,7 +310,14 @@ "successfulCreation": "Élément d'action créé avec succès", "successfulUpdation": "Élément d'action mis à jour avec succès", "successfulDeletion": "Élément d'action supprimé avec succès", - "title": "Éléments d'action" + "title": "Éléments d'action", + "category": "Catégorie", + "allotedHours": "Heures allouées", + "latestDueDate": "Date d'échéance la plus récente", + "earliestDueDate": "Date d'échéance la plus ancienne", + "updateActionItem": "Mettre à jour l'élément d'action", + "noneUpdated": "Aucun des champs n'a été mis à jour", + "updateStatusMsg": "Êtes-vous sûr de vouloir marquer cet élément d'action comme en attente?" }, "organizationAgendaCategory": { "agendaCategoryDetails": "Détails de la catégorie d'ordre du jour", @@ -604,10 +610,11 @@ "actionItemCategories": "Catégories d'éléments d'action", "updateOrganization": "Mettre à jour l'organisation", "seeRequest": "Voir la demande", - "noData": "Pas de données", - "otherSettings": "Autres réglages", + "noData": "Aucune donnée", + "otherSettings": "Autres paramètres", "changeLanguage": "Changer de langue", - "manageCustomFields": "Gérer les champs personnalisés" + "manageCustomFields": "Gérer les champs personnalisés", + "agendaItemCategories": "Catégories d'éléments d'agenda" }, "deleteOrg": { "deleteOrganization": "Supprimer l'organisation", @@ -938,13 +945,18 @@ "disableButton": "Désactiver", "updateActionItemCategory": "Mise à jour", "actionItemCategoryName": "Nom", - "actionItemCategoryDetails": "Détails de la catégorie d'élément d'action", + "categoryDetails": "Détails de la catégorie", "enterName": "Entrez le nom", "successfulCreation": "Catégorie d'élément d'action créée avec succès", "successfulUpdation": "Catégorie d'élément d'action mise à jour avec succès", "sameNameConflict": "Veuillez changer le nom pour effectuer une mise à jour", "categoryEnabled": "Catégorie d'élément d'action activée", - "categoryDisabled": "Catégorie d'élément d'action désactivée" + "categoryDisabled": "Catégorie d'élément d'action désactivée", + "noActionItemCategories": "Aucune catégorie d'élément d'action", + "status": "Statut", + "categoryDeleted": "Catégorie d'élément d'action supprimée avec succès", + "deleteCategory": "Supprimer la catégorie", + "deleteCategoryMsg": "Êtes-vous sûr de vouloir supprimer cette catégorie d'élément d'action ?" }, "organizationVenues": { "title": "Lieux", diff --git a/public/locales/hi/common.json b/public/locales/hi/common.json index 3889f73e59..c044033db5 100644 --- a/public/locales/hi/common.json +++ b/public/locales/hi/common.json @@ -82,5 +82,14 @@ "addedSuccessfully": "{{item}} सफलतापूर्वक जोड़ा गया", "updatedSuccessfully": "{{item}} सफलतापूर्वक अपडेट किया गया", "removedSuccessfully": "{{item}} सफलतापूर्वक हटाया गया", - "successfullyUpdated": "सफलतापूर्वक अपडेट किया गया" + "successfullyUpdated": "सफलतापूर्वक अपडेट किया गया", + "all": "सभी", + "active": "सक्रिय", + "disabled": "अक्षम", + "pending": "लंबित", + "completed": "पूरा हुआ", + "late": "देर से", + "createdLatest": "नवीनतम बनाया गया", + "createdEarliest": "सबसे पहले बनाया गया", + "searchBy": "के द्वारा खोजें {{item}}" } diff --git a/public/locales/hi/translation.json b/public/locales/hi/translation.json index 0de147b21a..7acb28ed80 100644 --- a/public/locales/hi/translation.json +++ b/public/locales/hi/translation.json @@ -286,7 +286,6 @@ "assignmentDate": "असाइनमेंट दिनांक", "active": "सक्रिय", "clearFilters": "फ़िल्टर साफ़ करें", - "completed": "पुरा होना।", "completionDate": "पूरा करने की तिथि", "createActionItem": "कार्रवाई आइटम बनाएं", "deleteActionItem": "क्रिया आइटम हटाएँ", @@ -311,7 +310,14 @@ "successfulCreation": "कार्रवाई आइटम सफलतापूर्वक बनाया गया", "successfulUpdation": "कार्रवाई आइटम सफलतापूर्वक अपडेट किया गया", "successfulDeletion": "कार्रवाई आइटम सफलतापूर्वक हटा दिया गया", - "title": "एक्शन आइटम्स" + "title": "एक्शन आइटम्स", + "category": "श्रेणी", + "allotedHours": "आवंटित घंटे", + "latestDueDate": "सबसे अधिक नियत तिथि", + "earliestDueDate": "सबसे पहले की नियत तिथि", + "updateActionItem": "कार्य आइटम अपडेट करें", + "noneUpdated": "कोई फ़ील्ड अपडेट नहीं किया गया", + "updateStatusMsg": "क्या आप वाकई इस कार्य आइटम को लंबित के रूप में चिह्नित करना चाहते हैं?" }, "organizationAgendaCategory": { "agendaCategoryDetails": "एजेंडा श्रेणी विवरण", @@ -599,15 +605,16 @@ "amount": "मात्रा" }, "orgSettings": { - "title": "समायोजन", + "title": "सेटिंग्स", "general": "सामान्य", - "actionItemCategories": "कार्रवाई आइटम श्रेणियाँ", - "updateOrganization": "संगठन अद्यतन करें", + "actionItemCategories": "कार्य आइटम श्रेणियाँ", + "updateOrganization": "संगठन अपडेट करें", "seeRequest": "अनुरोध देखें", "noData": "कोई डेटा नहीं", - "otherSettings": "अन्य सेटिंग", + "otherSettings": "अन्य सेटिंग्स", "changeLanguage": "भाषा बदलें", - "manageCustomFields": "कस्टम फ़ील्ड प्रबंधित करें" + "manageCustomFields": "कस्टम फ़ील्ड प्रबंधित करें", + "agendaItemCategories": "एजेंडा आइटम श्रेणियाँ" }, "deleteOrg": { "deleteOrganization": "संगठन हटाएँ", @@ -938,13 +945,18 @@ "disableButton": "अक्षम करना", "updateActionItemCategory": "अद्यतन", "actionItemCategoryName": "नाम", - "actionItemCategoryDetails": "कार्य आइटम श्रेणी विवरण", + "categoryDetails": "श्रेणी विवरण", "enterName": "नाम दर्ज करें", "successfulCreation": "कार्रवाई आइटम श्रेणी सफलतापूर्वक बनाई गई", "successfulUpdation": "कार्रवाई आइटम श्रेणी सफलतापूर्वक अपडेट की गई", "sameNameConflict": "अपडेट करने के लिए कृपया नाम बदलें", "categoryEnabled": "कार्य आइटम श्रेणी सक्षम", - "categoryDisabled": "कार्रवाई आइटम श्रेणी अक्षम" + "categoryDisabled": "कार्रवाई आइटम श्रेणी अक्षम", + "noActionItemCategories": "कोई कार्रवाई आइटम श्रेणी नहीं", + "status": "स्थिति", + "categoryDeleted": "कार्रवाई आइटम श्रेणी सफलतापूर्वक हटा दी गई", + "deleteCategory": "श्रेणी हटाएं", + "deleteCategoryMsg": "क्या आप वाकई इस कार्रवाई आइटम श्रेणी को हटाना चाहते हैं?" }, "organizationVenues": { "title": "स्थानों", diff --git a/public/locales/sp/common.json b/public/locales/sp/common.json index d784652da5..7e5871c914 100644 --- a/public/locales/sp/common.json +++ b/public/locales/sp/common.json @@ -82,5 +82,14 @@ "addedSuccessfully": "{{item}} agregado con éxito", "updatedSuccessfully": "{{item}} actualizado con éxito", "removedSuccessfully": "{{item}} eliminado con éxito", - "successfullyUpdated": "Actualizado con éxito" + "successfullyUpdated": "Actualizado con éxito", + "all": "Todos", + "active": "Activo", + "disabled": "Deshabilitado", + "pending": "Pendiente", + "completed": "Completado", + "late": "Tarde", + "createdLatest": "Creado más reciente", + "createdEarliest": "Creado más temprano", + "searchBy": "Buscar por {{item}}" } diff --git a/public/locales/sp/translation.json b/public/locales/sp/translation.json index d68701c261..e89f09269d 100644 --- a/public/locales/sp/translation.json +++ b/public/locales/sp/translation.json @@ -400,7 +400,6 @@ "active": "Activo", "clearFilters": "Borrar filtros", "close": "Cerrar", - "completed": "Completado", "completionDate": "Fecha de finalización", "createActionItem": "Crear ítem de acción", "deleteActionItem": "Eliminar ítem de acción", @@ -426,7 +425,14 @@ "successfulUpdation": "Ítem de acción actualizado con éxito", "successfulDeletion": "Ítem de acción eliminado con éxito", "title": "Ítems de acción", - "yes": "Sí" + "yes": "Sí", + "category": "Categoría", + "allotedHours": "Horas asignadas", + "latestDueDate": "Fecha de vencimiento más reciente", + "earliestDueDate": "Fecha de vencimiento más antigua", + "updateActionItem": "Actualizar elemento de acción", + "noneUpdated": "Ninguno de los campos se actualizó", + "updateStatusMsg": "¿Está seguro de que desea marcar este elemento de acción como pendiente?" }, "organizationAgendaCategory": { "agendaCategoryDetails": "Detalles de la categoría de la agenda", @@ -741,19 +747,16 @@ "amount": "Monto" }, "orgSettings": { - "title": "Configuración Talawa", - "pageName": "Configuración", + "title": "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", - "settings": "Ajustes", + "updateOrganization": "Actualizar organización", + "seeRequest": "Ver solicitud", "noData": "Sin datos", - "otherSettings": "Otras Configuraciones", - "changeLanguage": "Cambiar Idioma", - "manageCustomFields": "Gestionar Campos Personalizados" + "otherSettings": "Otras configuraciones", + "changeLanguage": "Cambiar idioma", + "manageCustomFields": "Administrar campos personalizados", + "agendaItemCategories": "Categorías de elementos de agenda" }, "deleteOrg": { "deleteOrganization": "Eliminar organización", @@ -1159,13 +1162,18 @@ "disableButton": "Inhabilitar", "updateActionItemCategory": "Actualizar", "actionItemCategoryName": "Nombre", - "actionItemCategoryDetails": "Detalles de la categoría de elemento de acción", + "categoryDetails": "Detalles de la categoría", "enterName": "Introduzca el nombre", "successfulCreation": "Categoría de elemento de acción creada correctamente", "successfulUpdation": "Categoría de elemento de acción actualizada correctamente", "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" + "categoryDisabled": "Categoría de elemento de acción deshabilitada", + "noActionItemCategories": "No hay categorías de elementos de acción", + "status": "Estado", + "categoryDeleted": "Categoría de elemento de acción eliminada con éxito", + "deleteCategory": "Eliminar categoría", + "deleteCategoryMsg": "¿Está seguro de que desea eliminar esta categoría de elemento de acción?" }, "organizationVenues": { "title": "Lugares", diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index 71b697b354..93659c644c 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -82,5 +82,14 @@ "addedSuccessfully": "{{item}} 添加成功", "updatedSuccessfully": "{{item}} 更新成功", "removedSuccessfully": "{{item}} 删除成功", - "successfullyUpdated": "更新成功" + "successfullyUpdated": "更新成功", + "all": "全部", + "active": "活跃", + "disabled": "禁用", + "pending": "待处理", + "completed": "已完成", + "late": "迟到", + "createdLatest": "最近创建", + "createdEarliest": "最早创建", + "searchBy": "搜索依据 {{item}}" } diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index 074c03dc0e..9914a27286 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -286,7 +286,6 @@ "assignmentDate": "分配日期", "active": "积极的", "clearFilters": "清除过滤器", - "completed": "完全的", "completionDate": "完成日期", "createActionItem": "创建操作项", "deleteActionItem": "删除操作项", @@ -311,7 +310,14 @@ "successfulCreation": "操作项创建成功", "successfulUpdation": "操作项已成功更新", "successfulDeletion": "操作项已成功删除", - "title": "行动项目" + "title": "行动项目", + "category": "类别", + "allotedHours": "分配的小时", + "latestDueDate": "最晚到期日", + "earliestDueDate": "最早到期日", + "updateActionItem": "更新操作项", + "noneUpdated": "没有更新任何字段", + "updateStatusMsg": "您确定要将此操作项标记为待处理吗?" }, "organizationAgendaCategory": { "agendaCategoryDetails": "议程类别详情", @@ -600,14 +606,15 @@ }, "orgSettings": { "title": "设置", - "general": "一般的", - "actionItemCategories": "行动项目类别", + "general": "一般", + "actionItemCategories": "操作项目类别", "updateOrganization": "更新组织", "seeRequest": "查看请求", "noData": "没有数据", "otherSettings": "其他设置", - "changeLanguage": "改变语言", - "manageCustomFields": "管理自定义字段" + "changeLanguage": "更改语言", + "manageCustomFields": "管理自定义字段", + "agendaItemCategories": "议程项目类别" }, "deleteOrg": { "deleteOrganization": "删除组织", @@ -938,13 +945,18 @@ "disableButton": "禁用", "updateActionItemCategory": "更新", "actionItemCategoryName": "姓名", - "actionItemCategoryDetails": "行动项目类别详细信息", + "categoryDetails": "类别详情", "enterName": "输入名字", "successfulCreation": "操作项类别创建成功", "successfulUpdation": "行动项目类别已成功更新", "sameNameConflict": "请更改名称以进行更新", "categoryEnabled": "已启用操作项类别", - "categoryDisabled": "操作项类别已禁用" + "categoryDisabled": "操作项类别已禁用", + "noActionItemCategories": "没有操作项目类别", + "status": "地位", + "categoryDeleted": "操作项目类别已成功删除", + "deleteCategory": "删除类别", + "deleteCategoryMsg": "您确定要删除此操作项目类别吗?" }, "organizationVenues": { "title": "场地", diff --git a/src/App.tsx b/src/App.tsx index df97e649d9..d6e825006f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,7 +14,6 @@ import OrgList from 'screens/OrgList/OrgList'; import OrgPost from 'screens/OrgPost/OrgPost'; import OrgSettings from 'screens/OrgSettings/OrgSettings'; import OrganizationActionItems from 'screens/OrganizationActionItems/OrganizationActionItems'; -import OrganizationAgendaCategory from 'screens/OrganizationAgendaCategory/OrganizationAgendaCategory'; import OrganizationDashboard from 'screens/OrganizationDashboard/OrganizationDashboard'; import OrganizationEvents from 'screens/OrganizationEvents/OrganizationEvents'; import OrganizaitionFundCampiagn from 'screens/OrganizationFundCampaign/OrganizationFundCampagins'; @@ -37,7 +36,6 @@ import Posts from 'screens/UserPortal/Posts/Posts'; import Organizations from 'screens/UserPortal/Organizations/Organizations'; import People from 'screens/UserPortal/People/People'; import Settings from 'screens/UserPortal/Settings/Settings'; -// import UserLoginPage from 'screens/UserPortal/UserLoginPage/UserLoginPage'; import Chat from 'screens/UserPortal/Chat/Chat'; import { useQuery } from '@apollo/client'; import { CHECK_AUTH } from 'GraphQl/Queries/Queries'; @@ -164,10 +162,6 @@ function app(): JSX.Element { path="/orgactionitems/:orgId" element={} /> - } - /> } /> - -angle-left - - \ No newline at end of file diff --git a/src/assets/svgs/eventDashboard.svg b/src/assets/svgs/eventDashboard.svg deleted file mode 100644 index 769c57d315..0000000000 --- a/src/assets/svgs/eventDashboard.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/assets/svgs/eventStats.svg b/src/assets/svgs/eventStats.svg deleted file mode 100644 index ffa43fdc4a..0000000000 --- a/src/assets/svgs/eventStats.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/components/ActionItems/ActionItemsContainer.module.css b/src/components/ActionItems/ActionItemsContainer.module.css deleted file mode 100644 index b55328c563..0000000000 --- a/src/components/ActionItems/ActionItemsContainer.module.css +++ /dev/null @@ -1,25 +0,0 @@ -.actionItemStatusBadge { - width: 5.5rem; - margin-left: 1.1rem; -} - -.createModal { - margin-top: 20vh; - margin-left: 13vw; - max-width: 80vw; -} - -.titlemodal { - color: var(--bs-gray-600); - font-weight: 600; - font-size: 20px; - margin-bottom: 20px; - padding-bottom: 5px; - border-bottom: 3px solid var(--bs-primary); - width: 65%; -} - -.actionItemsOptionsButton { - width: 24px; - height: 24px; -} diff --git a/src/components/ActionItems/ActionItemsContainer.test.tsx b/src/components/ActionItems/ActionItemsContainer.test.tsx deleted file mode 100644 index 7cb9c9e9bb..0000000000 --- a/src/components/ActionItems/ActionItemsContainer.test.tsx +++ /dev/null @@ -1,777 +0,0 @@ -import React from 'react'; -import { - render, - screen, - fireEvent, - waitFor, - act, - waitForElementToBeRemoved, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import 'jest-localstorage-mock'; -import { MockedProvider } from '@apollo/client/testing'; -import 'jest-location-mock'; -import { I18nextProvider } from 'react-i18next'; -import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; -import i18nForTest from 'utils/i18nForTest'; -import { toast } from 'react-toastify'; -import { LocalizationProvider } from '@mui/x-date-pickers'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; - -import { store } from 'state/store'; -import { StaticMockLink } from 'utils/StaticMockLink'; - -import ActionItemsContainer from './ActionItemsContainer'; -import { props, props2 } from './ActionItemsContainerProps'; -import { MOCKS, MOCKS_ERROR_MUTATIONS } from './ActionItemsContainerMocks'; - -const link = new StaticMockLink(MOCKS, true); -const link2 = new StaticMockLink(MOCKS_ERROR_MUTATIONS, true); - -jest.mock('react-toastify', () => ({ - toast: { - success: jest.fn(), - error: jest.fn(), - }, -})); - -jest.mock('@mui/x-date-pickers/DateTimePicker', () => { - return { - DateTimePicker: jest.requireActual( - '@mui/x-date-pickers/DesktopDateTimePicker', - ).DesktopDateTimePicker, - }; -}); - -async function wait(ms = 100): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); -} - -const translations = JSON.parse( - JSON.stringify( - i18nForTest.getDataByLanguage('en')?.translation.organizationActionItems, - ), -); - -describe('Testing Action Item Categories Component', () => { - const formData = { - assignee: 'Scott Norris', - preCompletionNotes: 'pre completion notes edited', - dueDate: '02/14/2024', - completionDate: '02/21/2024', - }; - - test('component loads correctly with action items', async () => { - render( - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.queryByText(translations.noActionItems), - ).not.toBeInTheDocument(); - }); - - expect(screen.getByText('#')).toBeInTheDocument(); - expect(screen.getByText(translations.assignee)).toBeInTheDocument(); - expect( - screen.getByText(translations.actionItemCategory), - ).toBeInTheDocument(); - expect( - screen.getByText(translations.preCompletionNotes), - ).toBeInTheDocument(); - expect( - screen.getByText(translations.postCompletionNotes), - ).toBeInTheDocument(); - - await wait(); - expect(screen.getAllByText('Harve Lance')[0]).toBeInTheDocument(); - - const asigneeAnchorElement = screen.getAllByText('Harve Lance')[0]; - expect(asigneeAnchorElement.tagName).toBe('A'); - expect(asigneeAnchorElement).toHaveAttribute('href', '/member/event1'); - - expect(screen.getAllByText('ActionItemCategory 1')[0]).toBeInTheDocument(); - const updateButtons = screen.getAllByTestId('editActionItemModalBtn'); - const previewButtons = screen.getAllByTestId('previewActionItemModalBtn'); - const updateStatusButtons = screen.getAllByTestId( - 'actionItemStatusChangeCheckbox', - ); - expect(updateButtons[0]).toBeInTheDocument(); - expect(previewButtons[0]).toBeInTheDocument(); - expect(updateStatusButtons[0]).toBeInTheDocument(); - }); - - test('component loads correctly with no action items', async () => { - render( - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.queryByText(translations.noActionItems), - ).toBeInTheDocument(); - }); - }); - - test('opens and closes the update modal correctly', async () => { - render( - - - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('editActionItemModalBtn')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('editActionItemModalBtn')[0]); - - await waitFor(() => { - return expect( - screen.findByTestId('updateActionItemModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('updateActionItemModalCloseBtn')); - - await waitForElementToBeRemoved(() => - screen.queryByTestId('updateActionItemModalCloseBtn'), - ); - }); - - test('opens and closes the action item status change modal correctly', async () => { - render( - - - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('actionItemStatusChangeCheckbox')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('actionItemStatusChangeCheckbox')[0]); - - await waitFor(() => { - return expect( - screen.findByTestId('actionItemStatusChangeModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('actionItemStatusChangeModalCloseBtn')); - - await waitForElementToBeRemoved(() => - screen.queryByTestId('actionItemStatusChangeModalCloseBtn'), - ); - }); - - test('completed action item status change modal loads correctly', async () => { - render( - - - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('actionItemStatusChangeCheckbox')[1], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('actionItemStatusChangeCheckbox')[1]); - - await waitFor(() => { - return expect( - screen.findByTestId('actionItemStatusChangeModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - expect(screen.getByText(translations.actionItemStatus)).toBeInTheDocument(); - - expect( - screen.getByTestId('actionItemsStatusChangeNotes'), - ).toBeInTheDocument(); - expect( - screen.getByPlaceholderText(translations.actionItemCompleted), - ).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: translations.makeActive }), - ).toBeInTheDocument(); - }); - - test('opens and closes the preview modal correctly', async () => { - render( - - - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('previewActionItemModalBtn')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('previewActionItemModalBtn')[0]); - - await waitFor(() => { - return expect( - screen.findByTestId('previewActionItemModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('previewActionItemModalCloseBtn')); - - await waitForElementToBeRemoved(() => - screen.queryByTestId('previewActionItemModalCloseBtn'), - ); - }); - - test('opens and closes the update and delete modals through the preview modal', async () => { - render( - - - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('previewActionItemModalBtn')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('previewActionItemModalBtn')[0]); - - await waitFor(() => { - return expect( - screen.findByTestId('previewActionItemModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - - await waitFor(() => { - expect( - screen.getByTestId('deleteActionItemPreviewModalBtn'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('deleteActionItemPreviewModalBtn')); - - await waitFor(() => { - return expect( - screen.findByTestId('actionItemDeleteModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('actionItemDeleteModalCloseBtn')); - - await waitForElementToBeRemoved(() => - screen.queryByTestId('actionItemDeleteModalCloseBtn'), - ); - - await waitFor(() => { - expect( - screen.getByTestId('editActionItemPreviewModalBtn'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('editActionItemPreviewModalBtn')); - - await waitFor(() => { - return expect( - screen.findByTestId('updateActionItemModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('updateActionItemModalCloseBtn')); - - await waitForElementToBeRemoved(() => - screen.queryByTestId('updateActionItemModalCloseBtn'), - ); - }); - - test('updates an action item and toasts success', async () => { - render( - - - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('editActionItemModalBtn')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('editActionItemModalBtn')[0]); - - await waitFor(() => { - expect(screen.getByTestId('formUpdateAssignee')).toBeInTheDocument(); - }); - - userEvent.selectOptions( - screen.getByTestId('formUpdateAssignee'), - formData.assignee, - ); - - const preCompletionNotes = screen.getByPlaceholderText( - translations.preCompletionNotes, - ); - fireEvent.change(preCompletionNotes, { target: { value: '' } }); - userEvent.type(preCompletionNotes, formData.preCompletionNotes); - - // const postCompletionNotes = screen.getByPlaceholderText( - // translations.postCompletionNotes, - // ); - // fireEvent.change(postCompletionNotes, { target: { value: '' } }); - // userEvent.type(postCompletionNotes, formData.postCompletionNotes); - - const dueDatePicker = screen.getByLabelText(translations.dueDate); - fireEvent.change(dueDatePicker, { - target: { value: formData.dueDate }, - }); - - const completionDatePicker = screen.getByLabelText( - translations.completionDate, - ); - fireEvent.change(completionDatePicker, { - target: { value: formData.completionDate }, - }); - - // await waitFor(() => { - // expect(screen.getByTestId('alldayCheck')).toBeInTheDocument(); - // }); - // userEvent.click(screen.getByTestId('alldayCheck')); - - await waitFor(() => { - expect(screen.getByTestId('editActionItemBtn')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('editActionItemBtn')); - - await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.successfulUpdation); - }); - }); - - test('toasts error on unsuccessful updation', async () => { - render( - - - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('editActionItemModalBtn')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('editActionItemModalBtn')[0]); - - await waitFor(() => { - expect(screen.getByTestId('formUpdateAssignee')).toBeInTheDocument(); - }); - - userEvent.selectOptions( - screen.getByTestId('formUpdateAssignee'), - formData.assignee, - ); - - const preCompletionNotes = screen.getByPlaceholderText( - translations.preCompletionNotes, - ); - fireEvent.change(preCompletionNotes, { target: { value: '' } }); - userEvent.type(preCompletionNotes, formData.preCompletionNotes); - - // const postCompletionNotes = screen.getByPlaceholderText( - // translations.postCompletionNotes, - // ); - // fireEvent.change(postCompletionNotes, { target: { value: '' } }); - // userEvent.type(postCompletionNotes, formData.postCompletionNotes); - - const dueDatePicker = screen.getByLabelText(translations.dueDate); - fireEvent.change(dueDatePicker, { - target: { value: formData.dueDate }, - }); - - const completionDatePicker = screen.getByLabelText( - translations.completionDate, - ); - fireEvent.change(completionDatePicker, { - target: { value: formData.completionDate }, - }); - - // await waitFor(() => { - // expect(screen.getByTestId('alldayCheck')).toBeInTheDocument(); - // }); - // userEvent.click(screen.getByTestId('alldayCheck')); - - await waitFor(() => { - expect(screen.getByTestId('editActionItemBtn')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('editActionItemBtn')); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); - }); - }); - - test('updates an action item status through the action item status change modal', async () => { - render( - - - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('actionItemStatusChangeCheckbox')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('actionItemStatusChangeCheckbox')[0]); - - await waitFor(() => { - expect( - screen.getByTestId('actionItemsStatusChangeNotes'), - ).toBeInTheDocument(); - }); - - const postCompletionNotes = screen.getByTestId( - 'actionItemsStatusChangeNotes', - ); - fireEvent.change(postCompletionNotes, { target: { value: '' } }); - userEvent.type( - postCompletionNotes, - 'this action item has been completed successfully', - ); - - await waitFor(() => { - expect( - screen.getByTestId('actionItemStatusChangeSubmitBtn'), - ).toBeInTheDocument(); - }); - expect( - screen.getByTestId('actionItemStatusChangeSubmitBtn'), - ).toHaveTextContent(translations.markCompletion); - userEvent.click(screen.getByTestId('actionItemStatusChangeSubmitBtn')); - - await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.successfulUpdation); - }); - - await waitFor(() => { - expect( - screen.getAllByTestId('actionItemStatusChangeCheckbox')[1], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('actionItemStatusChangeCheckbox')[1]); - - await waitFor(() => { - expect( - screen.getByTestId('actionItemsStatusChangeNotes'), - ).toBeInTheDocument(); - }); - - const preCompletionNotes = screen.getByTestId( - 'actionItemsStatusChangeNotes', - ); - fireEvent.change(preCompletionNotes, { target: { value: '' } }); - userEvent.type( - preCompletionNotes, - 'this action item has been made active again', - ); - - await waitFor(() => { - expect( - screen.getByTestId('actionItemStatusChangeSubmitBtn'), - ).toBeInTheDocument(); - }); - expect( - screen.getByTestId('actionItemStatusChangeSubmitBtn'), - ).toHaveTextContent(translations.makeActive); - userEvent.click(screen.getByTestId('actionItemStatusChangeSubmitBtn')); - - await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.successfulUpdation); - }); - }); - - test('deletes the action item and toasts success', async () => { - render( - - - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('previewActionItemModalBtn')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('previewActionItemModalBtn')[0]); - - await waitFor(() => { - return expect( - screen.findByTestId('previewActionItemModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - - await waitFor(() => { - expect( - screen.getByTestId('deleteActionItemPreviewModalBtn'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('deleteActionItemPreviewModalBtn')); - - await waitFor(() => { - return expect( - screen.findByTestId('actionItemDeleteModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - - userEvent.click(screen.getByTestId('deleteActionItemBtn')); - - await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.successfulDeletion); - }); - }); - - test('toasts error on unsuccessful deletion', async () => { - render( - - - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('previewActionItemModalBtn')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('previewActionItemModalBtn')[0]); - - await waitFor(() => { - return expect( - screen.findByTestId('previewActionItemModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - - await waitFor(() => { - expect( - screen.getByTestId('deleteActionItemPreviewModalBtn'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('deleteActionItemPreviewModalBtn')); - - await waitFor(() => { - return expect( - screen.findByTestId('actionItemDeleteModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('deleteActionItemBtn')); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); - }); - }); - - test('shows the overlay text on action item notes', async () => { - const { getAllByTestId } = render( - - - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('actionItemPreCompletionNotesOverlay')[0], - ).toBeInTheDocument(); - }); - - await waitFor(() => { - fireEvent.mouseEnter( - getAllByTestId('actionItemPreCompletionNotesOverlay')[0], - ); - }); - - await waitFor(() => { - expect(screen.getByTestId('popover-actionItem1')).toBeInTheDocument(); - }); - - await waitFor(() => { - fireEvent.mouseLeave( - getAllByTestId('actionItemPreCompletionNotesOverlay')[0], - ); - }); - - await waitFor(() => { - fireEvent.mouseEnter( - getAllByTestId('actionItemPostCompletionNotesOverlay')[0], - ); - }); - - await waitFor(() => { - expect(screen.getByTestId('popover-actionItem2')).toBeInTheDocument(); - }); - - await waitFor(() => { - fireEvent.mouseLeave( - getAllByTestId('actionItemPostCompletionNotesOverlay')[0], - ); - }); - }); - - test('Action Items loads with correct headers', async () => { - render( - - - - - - - - - , - ); - - await wait(); - - const actionItemHeaders = screen.getByTestId('actionItemsHeader'); - expect(actionItemHeaders).toBeInTheDocument(); - expect(screen.getByText(translations.assignee)).toBeInTheDocument(); - expect( - screen.getByText(translations.actionItemCategory), - ).toBeInTheDocument(); - expect( - screen.getByText(translations.preCompletionNotes), - ).toBeInTheDocument(); - expect( - screen.getByText(translations.postCompletionNotes), - ).toBeInTheDocument(); - expect(screen.getByText(translations.options)).toBeInTheDocument(); - }); -}); diff --git a/src/components/ActionItems/ActionItemsContainer.tsx b/src/components/ActionItems/ActionItemsContainer.tsx deleted file mode 100644 index fcee3b0083..0000000000 --- a/src/components/ActionItems/ActionItemsContainer.tsx +++ /dev/null @@ -1,568 +0,0 @@ -import React, { useState } from 'react'; -import dayjs from 'dayjs'; -import type { ChangeEvent } from 'react'; -import { - Button, - Col, - Form, - Modal, - OverlayTrigger, - Popover, - Row, -} from 'react-bootstrap'; -import { useTranslation } from 'react-i18next'; -import { toast } from 'react-toastify'; - -import { - DELETE_ACTION_ITEM_MUTATION, - UPDATE_ACTION_ITEM_MUTATION, -} from 'GraphQl/Mutations/mutations'; -import { useMutation } from '@apollo/client'; - -import type { - InterfaceActionItemInfo, - InterfaceMemberInfo, -} from 'utils/interfaces'; -import styles from './ActionItemsContainer.module.css'; -import ActionItemUpdateModal from '../../screens/OrganizationActionItems/ActionItemUpdateModal'; -import ActionItemPreviewModal from '../../screens/OrganizationActionItems/ActionItemPreviewModal'; -import ActionItemDeleteModal from '../../screens/OrganizationActionItems/ActionItemDeleteModal'; -import { Link } from 'react-router-dom'; - -/** - * ActionItemsContainer component is responsible for displaying, managing, and updating action items - * related to either an organization or an event. It provides a UI for previewing, updating, and deleting - * action items, as well as changing their status. - * - * @param props - The component props - * @param actionItemsConnection - Specifies the connection type (Organization or Event) to determine the context of the action items. - * @param actionItemsData - Array of action item data to be displayed. - * @param membersData - Array of member data for the organization. - * @param actionItemsRefetch - Function to refetch the action items data. - * - * @example - * ```tsx - * - * ``` - * This example renders the `ActionItemsContainer` component with organization connection, providing the necessary action items and members data along with a refetch function. - */ -function actionItemsContainer({ - actionItemsConnection, - actionItemsData, - membersData, - actionItemsRefetch, -}: { - actionItemsConnection: 'Organization' | 'Event'; - actionItemsData: InterfaceActionItemInfo[] | undefined; - membersData: InterfaceMemberInfo[] | undefined; - actionItemsRefetch: () => void; -}): JSX.Element { - // Translation hooks for localized text - const { t } = useTranslation('translation', { - keyPrefix: 'organizationActionItems', - }); - const { t: tCommon } = useTranslation('common'); - - // State hooks for controlling modals and action item properties - const [actionItemPreviewModalIsOpen, setActionItemPreviewModalIsOpen] = - useState(false); - const [actionItemUpdateModalIsOpen, setActionItemUpdateModalIsOpen] = - useState(false); - const [actionItemDeleteModalIsOpen, setActionItemDeleteModalIsOpen] = - useState(false); - const [actionItemStatusModal, setActionItemStatusModal] = useState(false); - const [isActionItemCompleted, setIsActionItemCompleted] = useState(false); - - const [assignmentDate, setAssignmentDate] = useState(new Date()); - const [dueDate, setDueDate] = useState(new Date()); - const [completionDate, setCompletionDate] = useState(new Date()); - const [actionItemId, setActionItemId] = useState(''); - const [actionItemNotes, setActionItemNotes] = useState(''); - - const [formState, setFormState] = useState({ - assignee: '', - assigner: '', - assigneeId: '', - preCompletionNotes: '', - postCompletionNotes: '', - isCompleted: false, - }); - - /** - * Opens the preview modal for the selected action item. - * - * @param actionItem - The action item to be previewed. - */ - const showPreviewModal = (actionItem: InterfaceActionItemInfo): void => { - setActionItemState(actionItem); - setActionItemPreviewModalIsOpen(true); - }; - - /** - * Toggles the update modal visibility. - */ - const showUpdateModal = (): void => { - setActionItemUpdateModalIsOpen(!actionItemUpdateModalIsOpen); - }; - - /** - * Hides the preview modal. - */ - const hidePreviewModal = (): void => { - setActionItemPreviewModalIsOpen(false); - }; - - /** - * Hides the update modal and resets the action item ID. - */ - const hideUpdateModal = (): void => { - setActionItemId(''); - setActionItemUpdateModalIsOpen(!actionItemUpdateModalIsOpen); - }; - - /** - * Toggles the delete modal visibility. - */ - const toggleDeleteModal = (): void => { - setActionItemDeleteModalIsOpen(!actionItemDeleteModalIsOpen); - }; - - // Apollo Client mutations for updating and deleting action items - const [updateActionItem] = useMutation(UPDATE_ACTION_ITEM_MUTATION); - - /** - * Handles the form submission for updating an action item. - * - * @param e - The form submission event. - */ - const updateActionItemHandler = async ( - e: ChangeEvent, - ): Promise => { - e.preventDefault(); - try { - await updateActionItem({ - variables: { - actionItemId, - assigneeId: formState.assigneeId, - preCompletionNotes: formState.preCompletionNotes, - postCompletionNotes: formState.postCompletionNotes, - dueDate: dayjs(dueDate).format('YYYY-MM-DD'), - completionDate: dayjs(completionDate).format('YYYY-MM-DD'), - isCompleted: formState.isCompleted, - }, - }); - - actionItemsRefetch(); - hideUpdateModal(); - toast.success(t('successfulUpdation') as string); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - console.log(error.message); - } - } - }; - - const [removeActionItem] = useMutation(DELETE_ACTION_ITEM_MUTATION); - - /** - * Handles the action item deletion. - */ - const deleteActionItemHandler = async (): Promise => { - try { - await removeActionItem({ - variables: { - actionItemId, - }, - }); - - actionItemsRefetch(); - toggleDeleteModal(); - toast.success(t('successfulDeletion') as string); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - console.log(error.message); - } - } - }; - - /** - * Handles the edit button click and opens the update modal with the action item data. - * - * @param actionItem - The action item to be edited. - */ - const handleEditClick = (actionItem: InterfaceActionItemInfo): void => { - setActionItemState(actionItem); - showUpdateModal(); - }; - - /** - * Handles the action item status change and updates the state accordingly. - * - * @param actionItem - The action item whose status is being changed. - */ - const handleActionItemStatusChange = ( - actionItem: InterfaceActionItemInfo, - ): void => { - actionItem = { ...actionItem, isCompleted: !actionItem.isCompleted }; - setIsActionItemCompleted(!actionItem.isCompleted); - setActionItemState(actionItem); - setActionItemStatusModal(true); - }; - - /** - * Hides the action item status modal. - */ - const hideActionItemStatusModal = (): void => { - setActionItemStatusModal(false); - }; - - /** - * Sets the state with the action item data. - * - * @param actionItem - The action item data. - */ - const setActionItemState = (actionItem: InterfaceActionItemInfo): void => { - setFormState({ - ...formState, - assignee: `${actionItem.assignee.firstName} ${actionItem.assignee.lastName}`, - assigner: `${actionItem.assigner.firstName} ${actionItem.assigner.lastName}`, - assigneeId: actionItem.assignee._id, - preCompletionNotes: actionItem.preCompletionNotes, - postCompletionNotes: actionItem.postCompletionNotes, - isCompleted: actionItem.isCompleted, - }); - setActionItemId(actionItem._id); - setDueDate(actionItem.dueDate); - setAssignmentDate(actionItem.assignmentDate); - setCompletionDate(actionItem.completionDate); - }; - - const popover = ( - - {actionItemNotes} - - ); - - return ( - <> -
-
- - -
{'#'}
- - -
{t('assignee')}
- - - {t('actionItemCategory')} - - -
{t('preCompletionNotes')}
- - -
{t('postCompletionNotes')}
- - -
{t('options')}
- -
-
- -
- {actionItemsData?.map((actionItem, index) => ( -
- - - {index + 1} - - - - {`${actionItem.assignee.firstName} ${actionItem.assignee.lastName}`} - - - - {actionItem.actionItemCategory.name} - - -
- - { - setActionItemId(actionItem._id); - setActionItemNotes(actionItem.preCompletionNotes); - }} - > - {actionItem.preCompletionNotes.length > 25 - ? `${actionItem.preCompletionNotes.substring(0, 25)}...` - : actionItem.preCompletionNotes} - - -
- - -
- {actionItem.isCompleted ? ( - - { - setActionItemId(actionItem._id); - setActionItemNotes(actionItem.postCompletionNotes); - }} - className="ms-3 " - > - {actionItem.postCompletionNotes?.length > 25 - ? `${actionItem.postCompletionNotes.substring(0, 25)}...` - : actionItem.postCompletionNotes} - - - ) : ( - - {t('actionItemActive')} - - )} -
- - -
- handleActionItemStatusChange(actionItem)} - /> - - -
- -
- - {index !== actionItemsData.length - 1 &&
} -
- ))} - - {actionItemsData?.length === 0 && ( -
- {t('noActionItems')} -
- )} -
-
- - {/* action item status change modal */} - - -

{t('actionItemStatus')}

- -
- -
- - {isActionItemCompleted - ? t('preCompletionNotes') - : t('postCompletionNotes')} - - { - if (isActionItemCompleted) { - setFormState({ - ...formState, - preCompletionNotes: e.target.value, - }); - } else { - setFormState({ - ...formState, - postCompletionNotes: e.target.value, - }); - } - }} - /> - - -
-
- - {/* preview modal */} - - - {/* Update Modal */} - - - {/* Delete Modal */} - - - ); -} - -export default actionItemsContainer; diff --git a/src/components/ActionItems/ActionItemsContainerMocks.ts b/src/components/ActionItems/ActionItemsContainerMocks.ts deleted file mode 100644 index a2dc20c5eb..0000000000 --- a/src/components/ActionItems/ActionItemsContainerMocks.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { - UPDATE_ACTION_ITEM_MUTATION, - DELETE_ACTION_ITEM_MUTATION, -} from 'GraphQl/Mutations/mutations'; - -export const MOCKS = [ - { - request: { - query: UPDATE_ACTION_ITEM_MUTATION, - variables: { - actionItemId: 'actionItem1', - assigneeId: 'user2', - preCompletionNotes: 'pre completion notes edited', - postCompletionNotes: 'Post Completion Notes', - dueDate: '2024-02-14', - completionDate: '2024-02-21', - isCompleted: false, - }, - }, - result: { - data: { - updateActionItem: { - _id: 'actionItem1', - }, - }, - }, - }, - { - request: { - query: UPDATE_ACTION_ITEM_MUTATION, - variables: { - actionItemId: 'actionItem1', - assigneeId: 'user1', - preCompletionNotes: 'Pre Completion Notes', - postCompletionNotes: 'this action item has been completed successfully', - dueDate: '2024-02-21', - completionDate: '2024-02-21', - isCompleted: true, - }, - }, - result: { - data: { - updateActionItem: { - _id: 'actionItem1', - }, - }, - }, - }, - { - request: { - query: UPDATE_ACTION_ITEM_MUTATION, - variables: { - actionItemId: 'actionItem2', - assigneeId: 'user1', - preCompletionNotes: 'this action item has been made active again', - postCompletionNotes: 'Post Completion Notes', - dueDate: '2024-02-21', - completionDate: '2024-02-21', - isCompleted: false, - }, - }, - result: { - data: { - updateActionItem: { - _id: 'actionItem1', - }, - }, - }, - }, - { - request: { - query: DELETE_ACTION_ITEM_MUTATION, - variables: { - actionItemId: 'actionItem1', - }, - }, - result: { - data: { - removeActionItem: { - _id: 'actionItem1', - }, - }, - }, - }, -]; - -export const MOCKS_ERROR_MUTATIONS = [ - { - request: { - query: UPDATE_ACTION_ITEM_MUTATION, - variables: { - actionItemId: 'actionItem1', - assigneeId: 'user2', - preCompletionNotes: 'pre completion notes edited', - postCompletionNotes: '', - dueDate: '2024-02-14', - completionDate: '2024-02-21', - isCompleted: false, - }, - }, - error: new Error('Mock Graphql Error'), - }, - { - request: { - query: DELETE_ACTION_ITEM_MUTATION, - variables: { - actionItemId: 'actionItem1', - }, - }, - error: new Error('Mock Graphql Error'), - }, -]; diff --git a/src/components/ActionItems/ActionItemsContainerProps.ts b/src/components/ActionItems/ActionItemsContainerProps.ts deleted file mode 100644 index 19ba6f6369..0000000000 --- a/src/components/ActionItems/ActionItemsContainerProps.ts +++ /dev/null @@ -1,131 +0,0 @@ -type ActionItemsConnectionType = 'Organization' | 'Event'; - -export const props = { - actionItemsConnection: 'Organization' as ActionItemsConnectionType, - actionItemsData: [ - { - _id: 'actionItem1', - assignee: { - _id: 'user1', - firstName: 'Harve', - lastName: 'Lance', - }, - actionItemCategory: { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - }, - preCompletionNotes: 'Pre Completion Notes', - postCompletionNotes: 'Post Completion Notes', - assignmentDate: new Date('2024-02-14'), - dueDate: new Date('2024-02-21'), - completionDate: new Date('2024-02-21'), - isCompleted: false, - assigner: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - _id: 'event1', - title: 'event 1', - }, - creator: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - }, - { - _id: 'actionItem2', - assignee: { - _id: 'user1', - firstName: 'Harve', - lastName: 'Lance', - }, - actionItemCategory: { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - }, - preCompletionNotes: 'Pre Completion Notes', - postCompletionNotes: 'Post Completion Notes', - assignmentDate: new Date('2024-02-14'), - dueDate: new Date('2024-02-21'), - completionDate: new Date('2024-02-21'), - isCompleted: true, - assigner: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - _id: 'event1', - title: 'event 1', - }, - creator: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - }, - { - _id: 'actionItem3', - assignee: { - _id: 'user1', - firstName: 'Harve', - lastName: 'Lance', - }, - actionItemCategory: { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - }, - preCompletionNotes: 'Pre Completion Notes more than 25 characters', - postCompletionNotes: 'Post Completion Notes more than 25 characters', - assignmentDate: new Date('2024-02-14'), - dueDate: new Date('2024-02-21'), - completionDate: new Date('2024-02-21'), - isCompleted: true, - assigner: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - _id: 'event1', - title: 'event 1', - }, - creator: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - }, - ], - membersData: [ - { - _id: 'user1', - firstName: 'Harve', - lastName: 'Lance', - email: 'harve@example.com', - image: '', - organizationsBlockedBy: [], - createdAt: '2024-02-14', - }, - { - _id: 'user2', - firstName: 'Scott', - lastName: 'Norris', - email: 'scott@example.com', - image: '', - organizationsBlockedBy: [], - createdAt: '2024-02-14', - }, - ], - actionItemsRefetch: jest.fn(), -}; - -export const props2 = { - actionItemsConnection: 'Organization' as ActionItemsConnectionType, - actionItemsData: [], - membersData: [], - actionItemsRefetch: jest.fn(), -}; diff --git a/src/components/ActionItems/ActionItemsModal.test.tsx b/src/components/ActionItems/ActionItemsModal.test.tsx deleted file mode 100644 index 01a15ee6aa..0000000000 --- a/src/components/ActionItems/ActionItemsModal.test.tsx +++ /dev/null @@ -1,294 +0,0 @@ -import React from 'react'; -import { - act, - fireEvent, - render, - screen, - waitFor, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { MockedProvider } from '@apollo/react-testing'; -import { ActionItemsModal } from './ActionItemsModal'; -import { BrowserRouter } from 'react-router-dom'; -import { Provider } from 'react-redux'; -import { store } from 'state/store'; -import { I18nextProvider } from 'react-i18next'; -import i18nForTest from 'utils/i18nForTest'; -import { LocalizationProvider } from '@mui/x-date-pickers'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { toast } from 'react-toastify'; - -import { - MOCKS_ERROR_ACTION_ITEM_CATEGORY_LIST_QUERY, - MOCKS_ERROR_ACTION_ITEM_LIST_QUERY, - MOCKS_ERROR_MEMBERS_LIST_QUERY, - MOCKS_ERROR_MUTATIONS, -} from '../../screens/OrganizationActionItems/OrganizationActionItemsErrorMocks'; -import { MOCKS } from '../../screens/OrganizationActionItems/OrganizationActionItemMocks'; -import { StaticMockLink } from 'utils/StaticMockLink'; - -jest.mock('react-toastify', () => ({ - toast: { - success: jest.fn(), - error: jest.fn(), - }, -})); - -jest.mock('@mui/x-date-pickers/DateTimePicker', () => { - return { - DateTimePicker: jest.requireActual( - '@mui/x-date-pickers/DesktopDateTimePicker', - ).DesktopDateTimePicker, - }; -}); - -async function wait(ms = 100): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); -} - -const link = new StaticMockLink(MOCKS, true); -const link2 = new StaticMockLink( - MOCKS_ERROR_ACTION_ITEM_CATEGORY_LIST_QUERY, - true, -); -const link3 = new StaticMockLink(MOCKS_ERROR_MEMBERS_LIST_QUERY, true); -const link4 = new StaticMockLink(MOCKS_ERROR_ACTION_ITEM_LIST_QUERY, true); -const link5 = new StaticMockLink(MOCKS_ERROR_MUTATIONS, true); - -const translations = JSON.parse( - JSON.stringify( - i18nForTest.getDataByLanguage('en')?.translation.organizationActionItems, - ), -); - -describe('Testing Check In Attendees Modal', () => { - const formData = { - actionItemCategory: 'ActionItemCategory 1', - assignee: 'Harve Lance', - preCompletionNotes: 'pre completion notes', - dueDate: '02/14/2024', - }; - - const props = { - show: true, - eventId: 'event1', - orgId: '123', - handleClose: jest.fn(), - }; - - test('The modal should be rendered properly', async () => { - render( - - - - - - - - - - - , - ); - - await waitFor(() => - expect(screen.queryByTestId('modal-title')).toBeInTheDocument(), - ); - - await waitFor(() => { - return expect( - screen.findByTestId('createEventActionItemBtn'), - ).resolves.toBeInTheDocument(); - }); - }); - - test('render error component on unsuccessful action item category list query', async () => { - const { queryByText } = render( - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect(queryByText('createEventActionItemBtn')).not.toBeInTheDocument(); - }); - }); - - test('render error component on unsuccessful member list query', async () => { - const { queryByText } = render( - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect(queryByText('createEventActionItemBtn')).not.toBeInTheDocument(); - }); - }); - - test('render error component on unsuccessful action items list query', async () => { - const { queryByText } = render( - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect(queryByText('createEventActionItemBtn')).not.toBeInTheDocument(); - }); - }); - - test('creates new action item associated with the event', async () => { - render( - - - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.getByTestId('createEventActionItemBtn'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('createEventActionItemBtn')); - - await waitFor(() => { - return expect( - screen.findByTestId('createActionItemModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - - await waitFor(() => { - expect( - screen.getByTestId('formSelectActionItemCategory'), - ).toBeInTheDocument(); - }); - - userEvent.selectOptions( - screen.getByTestId('formSelectActionItemCategory'), - formData.actionItemCategory, - ); - - userEvent.selectOptions( - screen.getByTestId('formSelectAssignee'), - formData.assignee, - ); - - userEvent.type( - screen.getByPlaceholderText(translations.preCompletionNotes), - formData.preCompletionNotes, - ); - - const dueDatePicker = screen.getByLabelText(translations.dueDate); - fireEvent.change(dueDatePicker, { - target: { value: formData.dueDate }, - }); - - userEvent.click(screen.getByTestId('createActionItemFormSubmitBtn')); - - await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.successfulCreation); - }); - }); - - test('toasts error on unsuccessful creation', async () => { - render( - - - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.getByTestId('createEventActionItemBtn'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('createEventActionItemBtn')); - - await waitFor(() => { - return expect( - screen.findByTestId('createActionItemModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - - await waitFor(() => { - expect( - screen.getByTestId('formSelectActionItemCategory'), - ).toBeInTheDocument(); - }); - - userEvent.selectOptions( - screen.getByTestId('formSelectActionItemCategory'), - formData.actionItemCategory, - ); - - userEvent.selectOptions( - screen.getByTestId('formSelectAssignee'), - formData.assignee, - ); - - userEvent.type( - screen.getByPlaceholderText(translations.preCompletionNotes), - formData.preCompletionNotes, - ); - - const dueDatePicker = screen.getByLabelText(translations.dueDate); - fireEvent.change(dueDatePicker, { - target: { value: formData.dueDate }, - }); - - userEvent.click(screen.getByTestId('createActionItemFormSubmitBtn')); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); - }); - }); -}); diff --git a/src/components/ActionItems/ActionItemsModal.tsx b/src/components/ActionItems/ActionItemsModal.tsx deleted file mode 100644 index f2e7c362a7..0000000000 --- a/src/components/ActionItems/ActionItemsModal.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import { Modal } from 'react-bootstrap'; -import styles from 'components/ActionItems/ActionItemsWrapper.module.css'; -import { ActionItemsModalBody } from './ActionItemsModalBody'; -import { useTranslation } from 'react-i18next'; - -/** - * Interface defining the props for the ActionItemsModal component. - */ -export interface InterfaceModalProp { - show: boolean; - eventId: string; - orgId: string; - handleClose: () => void; -} - -/** - * ActionItemsModal component displays a modal containing action items for a specific event within an organization. - * It includes a header with a title and a body that renders the ActionItemsModalBody component. - * - * @param props - The props for the ActionItemsModal component. - * @param show - Indicates whether the modal is visible. - * @param eventId - Event ID related to the action items. - * @param orgId - Organization ID related to the action items. - * @param handleClose - Function to handle closing the modal. - * - * - * @example - * ```tsx - * setShowModal(false)} - * /> - * ``` - * This example renders the `ActionItemsModal` component with the modal shown, using specific event and organization IDs, and a function to handle closing the modal. - */ -export const ActionItemsModal = (props: InterfaceModalProp): JSX.Element => { - const { show, eventId, orgId, handleClose } = props; - const { t } = useTranslation('translation', { - keyPrefix: 'organizationActionItems', - }); - - return ( - <> - - - - {t('eventActionItems')} - - - - - - - - ); -}; diff --git a/src/components/ActionItems/ActionItemsModalBody.tsx b/src/components/ActionItems/ActionItemsModalBody.tsx deleted file mode 100644 index 860dfbf6c3..0000000000 --- a/src/components/ActionItems/ActionItemsModalBody.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import React, { useState } from 'react'; -import type { ChangeEvent } from 'react'; -import { Button } from 'react-bootstrap'; -import { useMutation, useQuery } from '@apollo/client'; -import { - ACTION_ITEM_CATEGORY_LIST, - ACTION_ITEM_LIST, - MEMBERS_LIST, -} from 'GraphQl/Queries/Queries'; -import styles from 'components/ActionItems/ActionItemsWrapper.module.css'; -import type { - InterfaceActionItemCategoryList, - InterfaceActionItemList, - InterfaceMembersList, -} from 'utils/interfaces'; - -import ActionItemsContainer from 'components/ActionItems/ActionItemsContainer'; -import Loader from 'components/Loader/Loader'; -import { WarningAmberRounded } from '@mui/icons-material'; -import { CREATE_ACTION_ITEM_MUTATION } from 'GraphQl/Mutations/ActionItemMutations'; -import dayjs from 'dayjs'; -import { toast } from 'react-toastify'; -import ActionItemCreateModal from 'screens/OrganizationActionItems/ActionItemCreateModal'; -import { useTranslation } from 'react-i18next'; - -/** - * Component displaying the body of the Action Items modal. - * Fetches and displays action items, members, and action item categories related to a specific event within an organization. - * - * @param organizationId - The ID of the organization. - * @param eventId - The ID of the event. - * - * - * @example - * ```tsx - * - * ``` - * This example renders the `ActionItemsModalBody` component with the provided organization and event IDs. - */ -export const ActionItemsModalBody = ({ - organizationId, - eventId, -}: { - organizationId: string; - eventId: string; -}): JSX.Element => { - // Setting up translation - const { t } = useTranslation('translation', { - keyPrefix: 'organizationActionItems', - }); - const { t: tCommon } = useTranslation('common'); - - // State to manage due date - const [dueDate, setDueDate] = useState(new Date()); - // State to manage the visibility of the action item creation modal - const [actionItemCreateModalIsOpen, setActionItemCreateModalIsOpen] = - useState(false); - - // State to manage form inputs - const [formState, setFormState] = useState({ - actionItemCategoryId: '', - assigneeId: '', - preCompletionNotes: '', - }); - - // Query to fetch action item categories - const { - data: actionItemCategoriesData, - loading: actionItemCategoriesLoading, - error: actionItemCategoriesError, - }: { - data: InterfaceActionItemCategoryList | undefined; - loading: boolean; - error?: Error | undefined; - } = useQuery(ACTION_ITEM_CATEGORY_LIST, { - variables: { - organizationId, - }, - notifyOnNetworkStatusChange: true, - }); - - // Query to fetch members list - const { - data: membersData, - loading: membersLoading, - error: membersError, - }: { - data: InterfaceMembersList | undefined; - loading: boolean; - error?: Error | undefined; - } = useQuery(MEMBERS_LIST, { - variables: { id: organizationId }, - }); - - // Query to fetch action items list - const { - data: actionItemsData, - loading: actionItemsLoading, - error: actionItemsError, - refetch: actionItemsRefetch, - }: { - data: InterfaceActionItemList | undefined; - loading: boolean; - error?: Error | undefined; - refetch: () => void; - } = useQuery(ACTION_ITEM_LIST, { - variables: { - organizationId, - eventId, - orderBy: 'createdAt_DESC', - }, - notifyOnNetworkStatusChange: true, - }); - - // Mutation to create a new action item - const [createActionItem] = useMutation(CREATE_ACTION_ITEM_MUTATION); - - /** - * Handles the creation of a new action item. - * - * @param e - The change event from the form submission. - */ - const createActionItemHandler = async ( - e: ChangeEvent, - ): Promise => { - e.preventDefault(); - try { - await createActionItem({ - variables: { - assigneeId: formState.assigneeId, - actionItemCategoryId: formState.actionItemCategoryId, - eventId, - preCompletionNotes: formState.preCompletionNotes, - dueDate: dayjs(dueDate).format('YYYY-MM-DD'), - }, - }); - - // Resetting form state and due date after successful creation - setFormState({ - assigneeId: '', - actionItemCategoryId: '', - preCompletionNotes: '', - }); - - setDueDate(new Date()); - - // Refetching the action items list to update the UI - actionItemsRefetch(); - hideCreateModal(); - toast.success(t('successfulCreation') as string); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - console.log(error.message); - } - } - }; - - /** - * Shows the create action item modal. - */ - const showCreateModal = (): void => { - setActionItemCreateModalIsOpen(!actionItemCreateModalIsOpen); - }; - - /** - * Hides the create action item modal. - */ - const hideCreateModal = (): void => { - setActionItemCreateModalIsOpen(!actionItemCreateModalIsOpen); - }; - - // Showing loader while data is being fetched - if (actionItemCategoriesLoading || membersLoading || actionItemsLoading) { - return ; - } - - // Showing error message if any of the queries fail - if (actionItemCategoriesError || membersError || actionItemsError) { - return ( -
- -
- Error occured while loading{' '} - {actionItemCategoriesError - ? 'Action Item Categories' - : membersError - ? 'Members List' - : 'Action Items List'}{' '} - Data -
- {actionItemCategoriesError - ? actionItemCategoriesError.message - : membersError - ? membersError.message - : actionItemsError?.message} -
-
- ); - } - - // Filtering out disabled action item categories - const actionItemCategories = - actionItemCategoriesData?.actionItemCategoriesByOrganization.filter( - (category) => !category.isDisabled, - ); - - // Counting the number of completed action items - const completedActionItemsCount = - actionItemsData?.actionItemsByOrganization.reduce( - (acc, item) => (item.isCompleted === true ? acc + 1 : acc), - 0, - ); - - return ( - <> -
- - Status: - {actionItemsData?.actionItemsByOrganization.length} action items - assigned, {completedActionItemsCount} completed - - - -
- - - - {/* Create Modal */} - - - ); -}; diff --git a/src/components/ActionItems/ActionItemsWrapper.module.css b/src/components/ActionItems/ActionItemsWrapper.module.css deleted file mode 100644 index 125db8a125..0000000000 --- a/src/components/ActionItems/ActionItemsWrapper.module.css +++ /dev/null @@ -1,53 +0,0 @@ -.actionItemsModal { - margin: auto; - max-width: 85%; -} - -button .iconWrapper { - width: 32px; - padding-right: 4px; - margin-right: 4px; -} - -button .iconWrapperSm { - width: 32px; - display: flex; - justify-content: center; - align-items: center; -} - -.errorIcon { - transform: scale(1.5); - color: var(--bs-danger); - margin-bottom: 1rem; -} - -.greenregbtn { - margin: 1rem 0 0; - margin-top: 0; - margin-right: 4px; - border: 1px solid var(--bs-gray-300); - box-shadow: 0 2px 2px var(--bs-gray-300); - padding: 10px 10px; - border-radius: 5px; - background-color: var(--bs-primary); - width: 100%; - font-size: 16px; - color: var(--bs-white); - outline: none; - font-weight: 600; - cursor: pointer; - transition: - transform 0.2s, - box-shadow 0.2s; - max-width: 100px; -} - -.message { - margin-top: 15%; - margin-bottom: 15%; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -} diff --git a/src/components/ActionItems/ActionItemsWrapper.test.tsx b/src/components/ActionItems/ActionItemsWrapper.test.tsx deleted file mode 100644 index ad3cdc8687..0000000000 --- a/src/components/ActionItems/ActionItemsWrapper.test.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { act } from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; -import { MockedProvider } from '@apollo/react-testing'; -import { ActionItemsWrapper } from './ActionItemsWrapper'; -import { BrowserRouter } from 'react-router-dom'; -import { Provider } from 'react-redux'; -import { store } from 'state/store'; -import { I18nextProvider } from 'react-i18next'; -import i18nForTest from 'utils/i18nForTest'; -import { ToastContainer } from 'react-toastify'; -import { LocalizationProvider } from '@mui/x-date-pickers'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { StaticMockLink } from 'utils/StaticMockLink'; -import userEvent from '@testing-library/user-event'; -import { MOCKS } from '../../screens/OrganizationActionItems/OrganizationActionItemMocks'; - -async function wait(ms = 100): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); -} - -const link = new StaticMockLink(MOCKS, true); - -describe('Testing Action Items Wrapper', () => { - const props = { - eventId: 'event1', - orgId: '123', - }; - - test('The button to open and close the modal should work properly', async () => { - render( - - - - - - - - - - - - , - ); - - await wait(); - - await waitFor(() => { - expect( - screen.getByLabelText('eventDashboardActionItems'), - ).toBeInTheDocument(); - }); - - userEvent.click(screen.getByLabelText('eventDashboardActionItems')); - - await waitFor(() => - expect(screen.queryByTestId('modal-title')).toBeInTheDocument(), - ); - - await waitFor(() => { - expect(screen.getByLabelText('Close')).toBeInTheDocument(); - }); - - userEvent.click(screen.getByLabelText('Close')); - - await waitFor(() => - expect(screen.queryByTestId('modal-title')).not.toBeInTheDocument(), - ); - }); -}); diff --git a/src/components/ActionItems/ActionItemsWrapper.tsx b/src/components/ActionItems/ActionItemsWrapper.tsx deleted file mode 100644 index 16634637ad..0000000000 --- a/src/components/ActionItems/ActionItemsWrapper.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, { useState } from 'react'; -import { ActionItemsModal } from './ActionItemsModal'; -import { Button } from 'react-bootstrap'; -import IconComponent from 'components/IconComponent/IconComponent'; -import styles from './ActionItemsWrapper.module.css'; -import { useTranslation } from 'react-i18next'; - -type PropType = { - orgId: string; - eventId: string; -}; - -/** - * A React functional component that provides a button to open a modal for viewing and managing action items related to a specific event. - * - * This component displays a button that, when clicked, opens a modal dialog (`ActionItemsModal`). The modal allows users to interact with action items specific to the organization and event IDs passed as props. - * - * @param props - The props that define the organization's and event's context for the action items. - * @param orgId - The unique identifier for the organization. This ID is used to fetch and manage the organization's action items. - * @param eventId - The unique identifier for the event. This ID is used to fetch and manage the action items associated with the specific event. - * - * @returns The JSX element representing the action items button and modal. - * - * @example - * ```tsx - * - * ``` - * This example renders the `ActionItemsWrapper` component for an organization with ID "12345" and an event with ID "67890". The button will open a modal for managing action items related to this event. - */ -export const ActionItemsWrapper = ({ - orgId, - eventId, -}: PropType): JSX.Element => { - // Extract the translation function from the useTranslation hook, specifying the namespace and key prefix for translations - const { t } = useTranslation('translation', { - keyPrefix: 'organizationActionItems', - }); - - // State to control the visibility of the ActionItemsModal - const [showModal, setShowModal] = useState(false); - - return ( - <> - {/* Button to open the ActionItemsModal */} - - - {/* Conditionally render the ActionItemsModal if showModal is true */} - {showModal && ( - setShowModal(false)} // Function to close the modal - orgId={orgId} - eventId={eventId} - /> - )} - - ); -}; diff --git a/src/components/AgendaCategory/AgendaCategoryContainer.tsx b/src/components/AgendaCategory/AgendaCategoryContainer.tsx index 5fc038aee0..7b4c5cf8f4 100644 --- a/src/components/AgendaCategory/AgendaCategoryContainer.tsx +++ b/src/components/AgendaCategory/AgendaCategoryContainer.tsx @@ -12,9 +12,9 @@ import { import type { InterfaceAgendaItemCategoryInfo } from 'utils/interfaces'; import styles from './AgendaCategoryContainer.module.css'; -import AgendaCategoryDeleteModal from 'screens/OrganizationAgendaCategory/AgendaCategoryDeleteModal'; -import AgendaCategoryPreviewModal from 'screens/OrganizationAgendaCategory/AgendaCategoryPreviewModal'; -import AgendaCategoryUpdateModal from 'screens/OrganizationAgendaCategory/AgendaCategoryUpdateModal'; +import AgendaCategoryDeleteModal from 'components/OrgSettings/AgendaItemCategories/AgendaCategoryDeleteModal'; +import AgendaCategoryPreviewModal from 'components/OrgSettings/AgendaItemCategories/AgendaCategoryPreviewModal'; +import AgendaCategoryUpdateModal from 'components/OrgSettings/AgendaItemCategories/AgendaCategoryUpdateModal'; /** * Component for displaying and managing agenda item categories. diff --git a/src/components/EditCustomFieldDropDown/EditCustomFieldDropDown.test.tsx b/src/components/EditCustomFieldDropDown/EditCustomFieldDropDown.test.tsx index fc3431c7b2..19d2249a43 100644 --- a/src/components/EditCustomFieldDropDown/EditCustomFieldDropDown.test.tsx +++ b/src/components/EditCustomFieldDropDown/EditCustomFieldDropDown.test.tsx @@ -3,11 +3,11 @@ import React, { act } from 'react'; import { render } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; import EditOrgCustomFieldDropDown from './EditCustomFieldDropDown'; -import type { InterfaceCustomFieldData } from 'components/OrgProfileFieldSettings/OrgProfileFieldSettings'; import userEvent from '@testing-library/user-event'; import availableFieldTypes from 'utils/fieldTypes'; import { I18nextProvider } from 'react-i18next'; import i18nForTest from 'utils/i18nForTest'; +import type { InterfaceCustomFieldData } from 'utils/interfaces'; async function wait(ms = 100): Promise { await act(() => { diff --git a/src/components/EditCustomFieldDropDown/EditCustomFieldDropDown.tsx b/src/components/EditCustomFieldDropDown/EditCustomFieldDropDown.tsx index 4207e3f259..2350d2c9c4 100644 --- a/src/components/EditCustomFieldDropDown/EditCustomFieldDropDown.tsx +++ b/src/components/EditCustomFieldDropDown/EditCustomFieldDropDown.tsx @@ -2,8 +2,8 @@ import React from 'react'; import type { SetStateAction, Dispatch } from 'react'; import { Dropdown } from 'react-bootstrap'; import availableFieldTypes from 'utils/fieldTypes'; -import type { InterfaceCustomFieldData } from 'components/OrgProfileFieldSettings/OrgProfileFieldSettings'; import { useTranslation } from 'react-i18next'; +import type { InterfaceCustomFieldData } from 'utils/interfaces'; /** * Props for the EditOrgCustomFieldDropDown component. diff --git a/src/components/EventManagement/EventActionItems/EventActionItems.module.css b/src/components/EventManagement/EventActionItems/EventActionItems.module.css deleted file mode 100644 index 120177d155..0000000000 --- a/src/components/EventManagement/EventActionItems/EventActionItems.module.css +++ /dev/null @@ -1,206 +0,0 @@ -@media screen and (max-width: 575.5px) { - .mainpageright { - width: 98%; - } -} -.actionItemModal { - max-width: 80vw; - margin-top: 2vh; - margin-left: 13vw; -} -.modalContent { - width: 670px; - max-width: 680px; -} -.dropdown { - background-color: white; - border: 1px solid #31bb6b; - position: relative; - display: inline-block; - margin-top: 10px; - margin-bottom: 10px; - color: #31bb6b; -} -.input { - flex: 1; - position: relative; -} - -.btnsContainer { - display: flex; - margin: 2.5rem 0 2.5rem 0; -} - -.btnsContainer .btnsBlock { - display: flex; -} - -.btnsContainer .btnsBlock button { - margin-left: 1rem; - display: flex; - justify-content: center; - align-items: center; -} - -.btnsContainer .input { - flex: 1; - position: relative; -} - -/* input { - outline: 1px solid var(--bs-gray-400); -} */ - -.btnsContainer .input button { - width: 52px; -} - -.inputField { - margin-top: 10px; - margin-bottom: 10px; - background-color: white; - box-shadow: 0 1px 1px #31bb6b; -} -.inputFieldModal { - margin-bottom: 10px; - background-color: white; - box-shadow: 0 1px 1px #31bb6b; -} -.inputField > button { - padding-top: 10px; - padding-bottom: 10px; -} -.TableImage { - object-fit: cover; - width: 50px !important; - height: 50px !important; - border-radius: 100% !important; -} -.datagrid { - overflow: auto; - border-radius: 10px; -} -.tableHead { - background-color: #31bb6b !important; - color: white; - border-radius: 20px !important; - padding: 20px; - margin-top: 20px; -} - -.tableHead :nth-first-child() { - border-top-left-radius: 20px; -} - -.mainpageright > hr { - margin-top: 10px; - width: 100%; - margin-left: -15px; - margin-right: -15px; - margin-bottom: 20px; -} -.rowBackground { - background-color: var(--bs-white); -} -.tableHeader { - color: var(--bs-black); - font-size: var(--bs-body-font-size); -} -.addButton { - width: 7em; - position: absolute; - right: 1rem; - top: 1rem; -} - -.createModal { - margin-top: 20vh; - margin-left: 13vw; - max-width: 80vw; -} - -.icon { - transform: scale(1.5); - color: var(--bs-danger); - margin-bottom: 1rem; -} - -.message { - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -} - -.titlemodal { - color: var(--bs-gray-600); - font-weight: 600; - font-size: 20px; - margin-bottom: 20px; - padding-bottom: 5px; - border-bottom: 3px solid var(--bs-primary); - width: 65%; -} - -.editDelBtns { - margin-top: 15px; - border: 1px solid var(--bs-gray-300); - box-shadow: 0 2px 2px var(--bs-gray-300); - padding: 10px 10px; - border-radius: 5px; - background-color: var(--bs-primary); - width: 100%; - font-size: 16px; - color: var(--bs-white); - outline: none; - font-weight: 600; - cursor: pointer; - transition: - transform 0.2s, - box-shadow 0.2s; - width: 100%; -} - -.greenregbtn { - margin-left: 93%; -} - -.datatable { - margin-top: 5rem; -} - -.datediv { - display: flex; - flex-direction: row; -} -.datebox { - width: 90%; - border-radius: 7px; - outline: none; - box-shadow: none; -} -.actionItemsOptionsButton { - width: 24px; - height: 24px; -} - -@-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); - } -} diff --git a/src/components/EventManagement/EventActionItems/EventActionItems.test.tsx b/src/components/EventManagement/EventActionItems/EventActionItems.test.tsx deleted file mode 100644 index 3d1e5fa7bc..0000000000 --- a/src/components/EventManagement/EventActionItems/EventActionItems.test.tsx +++ /dev/null @@ -1,1178 +0,0 @@ -import React, { act } from 'react'; -import { MockedProvider } from '@apollo/react-testing'; -import { LocalizationProvider } from '@mui/x-date-pickers'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { - fireEvent, - render, - screen, - waitFor, - waitForElementToBeRemoved, -} from '@testing-library/react'; -import 'jest-localstorage-mock'; -import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; -import userEvent from '@testing-library/user-event'; -import { I18nextProvider } from 'react-i18next'; -import EventActionItems from './EventActionItems'; -import { store } from 'state/store'; -import 'jest-location-mock'; -import { toast } from 'react-toastify'; -import i18nForTest from 'utils/i18nForTest'; -import { StaticMockLink } from 'utils/StaticMockLink'; -import { - CREATE_ACTION_ITEM_MUTATION, - UPDATE_ACTION_ITEM_MUTATION, - DELETE_ACTION_ITEM_MUTATION, -} from 'GraphQl/Mutations/ActionItemMutations'; -import { - ACTION_ITEM_CATEGORY_LIST, - MEMBERS_LIST, -} from 'GraphQl/Queries/Queries'; -import { ACTION_ITEM_LIST_BY_EVENTS } from 'GraphQl/Queries/ActionItemQueries'; - -async function wait(ms = 100): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); -} - -jest.mock('react-toastify', () => ({ - toast: { - success: jest.fn(), - error: jest.fn(), - }, -})); - -const MOCKS = [ - { - request: { - query: CREATE_ACTION_ITEM_MUTATION, - variables: { - assigneeId: '658930fd2caa9d8d6908745c', - actionItemCategoryId: '65f069a53b63ad266db32b3f', - eventId: '123', - preCompletionNotes: 'task to be done with high priority', - dueDate: '2024-04-05', - }, - }, - result: { - data: { - createActionItem: { - _id: 'newly_created_action_item_id', - }, - }, - }, - }, - { - request: { - query: UPDATE_ACTION_ITEM_MUTATION, - variables: { - actionItemId: '_6613ef741677gygwuyu', - assigneeId: '658930fd2caa9d8d690sfhgush', - preCompletionNotes: 'pre completion notes edited', - postCompletionNotes: 'Post Completion Notes', - dueDate: '2024-02-14', - completionDate: '2024-02-21', - isCompleted: false, - }, - }, - result: { - data: { - updateActionItem: { - _id: '_6613ef741677gygwuyu', - }, - }, - }, - }, - { - request: { - query: UPDATE_ACTION_ITEM_MUTATION, - variables: { - actionItemId: '_6613ef741677gygwuyu', - assigneeId: '658930fd2caa9d8d6908745c', - preCompletionNotes: 'Pre Completion Notes', - postCompletionNotes: 'this action item has been completed successfully', - dueDate: '2024-02-21', - completionDate: '2024-02-21', - isCompleted: true, - }, - }, - result: { - data: { - updateActionItem: { - _id: '_6613ef741677gygwuyu', - }, - }, - }, - }, - { - request: { - query: UPDATE_ACTION_ITEM_MUTATION, - variables: { - actionItemId: 'actionItem2', - assigneeId: 'user1', - preCompletionNotes: 'this action item has been made active again', - postCompletionNotes: 'Post Completion Notes', - dueDate: '2024-02-21', - completionDate: '2024-02-21', - isCompleted: false, - }, - }, - result: { - data: { - updateActionItem: { - _id: '_6613ef741677gygwuyu', - }, - }, - }, - }, - { - request: { - query: DELETE_ACTION_ITEM_MUTATION, - variables: { - actionItemId: '_6613ef741677gygwuyu', - }, - }, - result: { - data: { - removeActionItem: { - _id: '_6613ef741677gygwuyu', - __typename: 'ActionItem', - }, - }, - }, - }, - { - request: { - query: ACTION_ITEM_CATEGORY_LIST, - variables: { - organizationId: '111', - }, - }, - result: { - data: { - actionItemCategoriesByOrganization: [ - { - _id: '65f069a53b63ad266db32b3f', - name: 'Default', - isDisabled: false, - __typename: 'ActionItemCategory', - }, - ], - }, - }, - }, - { - request: { - query: MEMBERS_LIST, - variables: { - id: '111', - }, - }, - result: { - data: { - organizations: [ - { - _id: '111', - members: [ - { - createdAt: '2023-04-13', - email: 'testuser4@example.com', - firstName: 'Teresa', - image: null, - lastName: 'Bradley', - organizationsBlockedBy: [], - __typename: 'User', - _id: '658930fd2caa9d8d6908745c', - }, - { - createdAt: '2024-04-13', - email: 'testuser2@example.com', - firstName: 'Anna', - image: null, - lastName: 'Bradley', - organizationsBlockedBy: [], - __typename: 'User', - _id: '658930fd2caa9d8d690sfhgush', - }, - ], - }, - ], - }, - }, - }, - { - request: { - query: ACTION_ITEM_LIST_BY_EVENTS, - variables: { - eventId: '123', - }, - }, - result: { - data: { - actionItemsByEvent: [ - { - _id: '_6613ef741677gygwuyu', - actionItemCategory: { - __typename: 'ActionItemCategory', - _id: '65f069a53b63ad266db32b3j', - name: 'Default', - }, - assignee: { - __typename: 'User', - _id: '658930fd2caa9d8d6908745c', - firstName: 'Burton', - lastName: 'Sanders', - }, - assigner: { - __typename: 'User', - _id: '64378abd85008f171cf2990d', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - assignmentDate: new Date('2024-02-14'), - dueDate: new Date('2024-02-21'), - completionDate: new Date('2024-02-21'), - creator: { - __typename: 'User', - _id: '64378abd85008f171cf2990d', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - __typename: 'Event', - _id: '123', - title: 'Adult Painting Lessons', - }, - isCompleted: false, - postCompletionNotes: 'Post Completion Notes', - preCompletionNotes: 'Pre Completion Notes', - }, - { - _id: 'actionItem2', - assignee: { - _id: '658930fd2caa9d8d6908745c', - firstName: 'Harve', - lastName: 'Lance', - }, - actionItemCategory: { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - }, - preCompletionNotes: - 'Long Pre Completion Notes Text that exceeds 25 characters', - postCompletionNotes: - 'Long Post Completion Notes Text that exceeds 25 characters', - assignmentDate: new Date('2024-02-14'), - dueDate: new Date('2024-02-21'), - completionDate: new Date('2024-02-21'), - isCompleted: true, - assigner: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - _id: 'event1', - title: 'event 1', - }, - creator: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - }, - { - _id: 'actionItem3', - assignee: { - _id: '658930fd2caa9d8d6908745c', - firstName: 'Harve', - lastName: 'Lance', - }, - actionItemCategory: { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - }, - preCompletionNotes: 'Pre Completion Text', - postCompletionNotes: 'Post Completion Text', - assignmentDate: new Date('2024-02-14'), - dueDate: new Date('2024-02-21'), - completionDate: new Date('2024-02-21'), - isCompleted: true, - assigner: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - _id: 'event1', - title: 'event 1', - }, - creator: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - }, - ], - }, - refetch: jest.fn(), - }, - }, -]; - -const CREATE_ACTION_ITEM_ERROR_MOCK = [ - { - request: { - query: CREATE_ACTION_ITEM_MUTATION, - variables: { - assigneeId: '658930fd2caa9d8d6908745c', - actionItemCategoryId: '65f069a53b63ad266db32b3f', - eventId: '123', - preCompletionNotes: 'task to be done with high priority', - dueDate: '2024-04-05', - }, - }, - result: { - data: { - createActionItem: { - _id: undefined, - }, - }, - }, - }, -]; - -const UPDATE_ACTION_ITEM_ERROR_MOCK = [ - { - request: { - query: ACTION_ITEM_LIST_BY_EVENTS, - variables: { - eventId: '123', - }, - }, - result: { - data: { - actionItemsByEvent: [ - { - _id: '_6613ef741677gygwuyu', - actionItemCategory: { - __typename: 'ActionItemCategory', - _id: '65f069a53b63ad266db32b3j', - name: 'Default', - }, - assignee: { - __typename: 'User', - _id: '658930fd2caa9d8d6908745c', - firstName: 'Burton', - lastName: 'Sanders', - }, - assigner: { - __typename: 'User', - _id: '64378abd85008f171cf2990d', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - assignmentDate: new Date('2024-02-14'), - dueDate: new Date('2024-02-21'), - completionDate: new Date('2024-02-21'), - creator: { - __typename: 'User', - _id: '64378abd85008f171cf2990d', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - __typename: 'Event', - _id: '123', - title: 'Adult Painting Lessons', - }, - isCompleted: false, - postCompletionNotes: 'Post Completion Note', - preCompletionNotes: 'Pre Completion Note', - }, - ], - }, - refetch: jest.fn(), - }, - }, - { - request: { - query: MEMBERS_LIST, - variables: { - id: '111', - }, - }, - result: { - data: { - organizations: [ - { - _id: '111', - members: [ - { - createdAt: '2023-04-13', - email: 'testuser4@example.com', - firstName: 'Teresa', - image: null, - lastName: 'Bradley', - organizationsBlockedBy: [], - __typename: 'User', - _id: '658930fd2caa9d8d6908745c', - }, - { - createdAt: '2024-04-13', - email: 'testuser2@example.com', - firstName: 'Anna', - image: null, - lastName: 'Bradley', - organizationsBlockedBy: [], - __typename: 'User', - _id: '658930fd2caa9d8d690sfhgush', - }, - ], - }, - ], - }, - }, - }, - { - request: { - query: DELETE_ACTION_ITEM_MUTATION, - variables: { - actionItemId: '_6613ef741677gygwuyu', - }, - }, - error: new Error('Mock Graphql Error'), - }, - { - request: { - query: UPDATE_ACTION_ITEM_MUTATION, - variables: { - actionItemId: '_6613ef741677gygwuyu', - assigneeId: '658930fd2caa9d8d690sfhgush', - preCompletionNotes: 'pre completion notes edited', - postCompletionNotes: '', - dueDate: '2024-02-14', - completionDate: '2024-02-21', - isCompleted: false, - }, - }, - error: new Error('Mock Graphql Error'), - }, -]; - -const NO_ACTION_ITEMS_ERROR_MOCK = [ - { - request: { - query: ACTION_ITEM_LIST_BY_EVENTS, - variables: { - eventId: '123', - }, - }, - result: { - data: { - actionItemsByEvent: [], - }, - refetch: jest.fn(), - }, - }, -]; - -const link = new StaticMockLink(MOCKS, true); -const link2 = new StaticMockLink(CREATE_ACTION_ITEM_ERROR_MOCK, true); -const link3 = new StaticMockLink(UPDATE_ACTION_ITEM_ERROR_MOCK, true); -const link4 = new StaticMockLink(NO_ACTION_ITEMS_ERROR_MOCK, true); - -const translations = JSON.parse( - JSON.stringify( - i18nForTest.getDataByLanguage('en')?.translation.eventActionItems, - ), -); - -describe('Event Action Items Page', () => { - const formData = { - assignee: 'Anna Bradley', - preCompletionNotes: 'pre completion notes edited', - dueDate: '02/14/2024', - completionDate: '02/21/2024', - }; - test('Testing add new action item modal', async () => { - window.location.assign('/event/111/123'); - render( - - - - - - {} - - - - - , - ); - await wait(); - userEvent.click(screen.getByTestId('createEventActionItemBtn')); - - await wait(); - expect( - screen.getByText(translations.actionItemDetails), - ).toBeInTheDocument(); - - const categoryDropdown = screen.getByTestId('formSelectActionItemCategory'); - userEvent.selectOptions(categoryDropdown, 'Default'); - - expect(categoryDropdown).toHaveValue('65f069a53b63ad266db32b3f'); - - const assigneeDropdown = screen.getByTestId('formSelectAssignee'); - userEvent.selectOptions(assigneeDropdown, 'Teresa Bradley'); - - expect(assigneeDropdown).toHaveValue('658930fd2caa9d8d6908745c'); - - fireEvent.change( - screen.getByPlaceholderText(translations.preCompletionNotes), - { - target: { value: 'task to be done with high priority' }, - }, - ); - expect( - screen.getByPlaceholderText(translations.preCompletionNotes), - ).toHaveValue('task to be done with high priority'); - - fireEvent.change(screen.getByLabelText(translations.dueDate), { - target: { value: '04/05/2024' }, - }); - expect(screen.getByLabelText(translations.dueDate)).toHaveValue( - '04/05/2024', - ); - - userEvent.click(screen.getByTestId('createActionItemFormSubmitBtn')); - - await wait(); - - expect(toast.success).toBeCalledWith(translations.successfulCreation); - }); - - test('Display all the action items', async () => { - window.location.assign('/event/111/123'); - render( - - - - - - {} - - - - - , - ); - await wait(); - - expect(screen.getByText('#')).toBeInTheDocument(); - expect(screen.getByText(translations.assignee)).toBeInTheDocument(); - expect( - screen.getByText(translations.actionItemCategory), - ).toBeInTheDocument(); - expect( - screen.getByText(translations.preCompletionNotes), - ).toBeInTheDocument(); - expect( - screen.getByText(translations.postCompletionNotes), - ).toBeInTheDocument(); - - await wait(); - const asigneeAnchorElement = screen.getByText('Burton Sanders'); - expect(asigneeAnchorElement.tagName).toBe('A'); - expect(asigneeAnchorElement).toHaveAttribute('href', '/member/123'); - - expect(screen.getByText('Burton Sanders')).toBeInTheDocument(); - const updateButtons = screen.getAllByTestId('editActionItemModalBtn'); - const previewButtons = screen.getAllByTestId('previewActionItemModalBtn'); - const updateStatusButtons = screen.getAllByTestId( - 'actionItemStatusChangeCheckbox', - ); - expect(updateButtons[0]).toBeInTheDocument(); - expect(previewButtons[0]).toBeInTheDocument(); - expect(updateStatusButtons[0]).toBeInTheDocument(); - - // Truncate notes and long completion notes txt - expect( - screen.getAllByTestId('actionItemPreCompletionNotesOverlay')[1], - ).toHaveTextContent('Long Pre Completion Notes...'); - expect( - screen.getAllByTestId('actionItemPostCompletionNotesOverlay')[0], - ).toHaveTextContent('Long Post Completion Note...'); - expect( - screen.getAllByTestId('actionItemPostCompletionNotesOverlay')[1], - ).toHaveTextContent('Post Completion Text'); - expect( - screen.getAllByTestId('actionItemPreCompletionNotesOverlay')[2], - ).toHaveTextContent('Pre Completion Text'); - }); - - test('opens and closes the update and delete modals through the preview modal', async () => { - window.location.assign('/event/111/123'); - render( - - - - - - {} - - - - - , - ); - - await wait(); - - expect( - screen.getAllByTestId('previewActionItemModalBtn')[0], - ).toBeInTheDocument(); - userEvent.click(screen.getAllByTestId('previewActionItemModalBtn')[0]); - - await waitFor(() => { - return expect( - screen.findByTestId('previewActionItemModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - - await waitFor(() => { - expect( - screen.getByTestId('deleteActionItemPreviewModalBtn'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('deleteActionItemPreviewModalBtn')); - - await waitFor(() => { - return expect( - screen.findByTestId('actionItemDeleteModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('actionItemDeleteModalCloseBtn')); - - await waitForElementToBeRemoved(() => - screen.queryByTestId('actionItemDeleteModalCloseBtn'), - ); - - expect( - screen.getByTestId('editActionItemPreviewModalBtn'), - ).toBeInTheDocument(); - userEvent.click(screen.getByTestId('editActionItemPreviewModalBtn')); - - await waitFor(() => { - return expect( - screen.findByTestId('updateActionItemModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('updateActionItemModalCloseBtn')); - - await waitForElementToBeRemoved(() => - screen.queryByTestId('updateActionItemModalCloseBtn'), - ); - }); - test('opens and closes the action item status change modal correctly', async () => { - window.location.assign('/event/111/123'); - render( - - - - - - {} - - - - - , - ); - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('actionItemStatusChangeCheckbox')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('actionItemStatusChangeCheckbox')[0]); - - await waitFor(() => { - return expect( - screen.findByTestId('actionItemStatusChangeModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('actionItemStatusChangeModalCloseBtn')); - - await waitForElementToBeRemoved(() => - screen.queryByTestId('actionItemStatusChangeModalCloseBtn'), - ); - }); - - test('updates an action item status through the action item status change modal', async () => { - window.location.assign('/event/111/123'); - render( - - - - - - {} - - - - - , - ); - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('actionItemStatusChangeCheckbox')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('actionItemStatusChangeCheckbox')[0]); - - await waitFor(() => { - expect( - screen.getByTestId('actionItemsStatusChangeNotes'), - ).toBeInTheDocument(); - }); - - const postCompletionNotes = screen.getByTestId( - 'actionItemsStatusChangeNotes', - ); - fireEvent.change(postCompletionNotes, { target: { value: '' } }); - userEvent.type( - postCompletionNotes, - 'this action item has been completed successfully', - ); - - await waitFor(() => { - expect( - screen.getByTestId('actionItemStatusChangeSubmitBtn'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('actionItemStatusChangeSubmitBtn')); - - await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.successfulUpdation); - }); - - await waitFor(() => { - expect( - screen.getAllByTestId('actionItemStatusChangeCheckbox')[1], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('actionItemStatusChangeCheckbox')[1]); - - await waitFor(() => { - expect( - screen.getByTestId('actionItemsStatusChangeNotes'), - ).toBeInTheDocument(); - }); - - const preCompletionNotes = screen.getByTestId( - 'actionItemsStatusChangeNotes', - ); - fireEvent.change(preCompletionNotes, { target: { value: '' } }); - userEvent.type( - preCompletionNotes, - 'this action item has been made active again', - ); - - await waitFor(() => { - expect( - screen.getByTestId('actionItemStatusChangeSubmitBtn'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('actionItemStatusChangeSubmitBtn')); - - await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.successfulUpdation); - }); - }); - - test('Testing update action item modal', async () => { - window.location.assign('/event/111/123'); - render( - - - - - - {} - - - - - , - ); - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('editActionItemModalBtn')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('editActionItemModalBtn')[0]); - - await waitFor(() => { - expect(screen.getByTestId('formUpdateAssignee')).toBeInTheDocument(); - }); - - userEvent.selectOptions( - screen.getByTestId('formUpdateAssignee'), - formData.assignee, - ); - - const preCompletionNotes = screen.getByPlaceholderText( - translations.preCompletionNotes, - ); - fireEvent.change(preCompletionNotes, { target: { value: '' } }); - userEvent.type(preCompletionNotes, formData.preCompletionNotes); - - const dueDatePicker = screen.getByLabelText(translations.dueDate); - fireEvent.change(dueDatePicker, { - target: { value: formData.dueDate }, - }); - - const completionDatePicker = screen.getByLabelText( - translations.completionDate, - ); - fireEvent.change(completionDatePicker, { - target: { value: formData.completionDate }, - }); - - await waitFor(() => { - expect( - screen.getByTestId('updateActionItemFormSubmitBtn'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('updateActionItemFormSubmitBtn')); - - await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.successfulUpdation); - }); - }); - test('Testing delete action item modal and delete the record', async () => { - window.location.assign('/event/111/123'); - render( - - - - - - {} - - - - - , - ); - await wait(); - expect( - screen.getAllByTestId('previewActionItemModalBtn')[0], - ).toBeInTheDocument(); - userEvent.click(screen.getAllByTestId('previewActionItemModalBtn')[0]); - - await waitFor(() => { - expect( - screen.getByTestId('deleteActionItemPreviewModalBtn'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('deleteActionItemPreviewModalBtn')); - await wait(); - expect( - screen.getByText(translations.deleteActionItemMsg), - ).toBeInTheDocument(); - userEvent.click(screen.getByText('Yes')); - await wait(); - - expect(toast.success).toBeCalledWith(translations.successfulDeletion); - }); - - test('Testing delete action item modal and does not delete the record', async () => { - window.location.assign('/event/111/123'); - render( - - - - - - {} - - - - - , - ); - await wait(); - expect( - screen.getAllByTestId('previewActionItemModalBtn')[0], - ).toBeInTheDocument(); - userEvent.click(screen.getAllByTestId('previewActionItemModalBtn')[0]); - - await waitFor(() => { - expect( - screen.getByTestId('deleteActionItemPreviewModalBtn'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('deleteActionItemPreviewModalBtn')); - await wait(); - expect( - screen.getByText(translations.deleteActionItemMsg), - ).toBeInTheDocument(); - userEvent.click(screen.getByText('No')); - await wait(); - expect(screen.getByRole('dialog')).toBeInTheDocument(); - expect( - screen.getByText(translations.actionItemDetails), - ).toBeInTheDocument(); - }); - - test('toasts error on unsuccessful deletion', async () => { - window.location.assign('/event/111/123'); - render( - - - - - - {} - - - - - , - ); - await wait(); - - expect( - screen.getAllByTestId('previewActionItemModalBtn')[0], - ).toBeInTheDocument(); - userEvent.click(screen.getAllByTestId('previewActionItemModalBtn')[0]); - - await waitFor(() => { - return expect( - screen.findByTestId('previewActionItemModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - - await waitFor(() => { - expect( - screen.getByTestId('deleteActionItemPreviewModalBtn'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('deleteActionItemPreviewModalBtn')); - - await waitFor(() => { - return expect( - screen.findByTestId('actionItemDeleteModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('deleteActionItemBtn')); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); - }); - }); - - test('toasts error on unsuccessful updation', async () => { - window.location.assign('/event/111/123'); - render( - - - - - - {} - - - - - , - ); - await wait(); - - await waitFor(() => { - expect( - screen.getAllByTestId('editActionItemModalBtn')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('editActionItemModalBtn')[0]); - - await waitFor(() => { - expect(screen.getByTestId('formUpdateAssignee')).toBeInTheDocument(); - }); - - userEvent.selectOptions( - screen.getByTestId('formUpdateAssignee'), - formData.assignee, - ); - - const preCompletionNotes = screen.getByPlaceholderText( - translations.preCompletionNotes, - ); - fireEvent.change(preCompletionNotes, { target: { value: '' } }); - userEvent.type(preCompletionNotes, formData.preCompletionNotes); - - const dueDatePicker = screen.getByLabelText(translations.dueDate); - fireEvent.change(dueDatePicker, { - target: { value: formData.dueDate }, - }); - - const completionDatePicker = screen.getByLabelText( - translations.completionDate, - ); - fireEvent.change(completionDatePicker, { - target: { value: formData.completionDate }, - }); - - await waitFor(() => { - expect( - screen.getByTestId('updateActionItemFormSubmitBtn'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('updateActionItemFormSubmitBtn')); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); - }); - }); - - test('Raises an error when incorrect information is filled while creation', async () => { - window.location.assign('/event/111/123'); - render( - - - - - - {} - - - - - , - ); - await wait(); - userEvent.click(screen.getByTestId('createEventActionItemBtn')); - - await wait(); - expect( - screen.getByText(translations.actionItemDetails), - ).toBeInTheDocument(); - - fireEvent.change( - screen.getByPlaceholderText(translations.preCompletionNotes), - { - target: { value: 'task to be done with high priority' }, - }, - ); - expect( - screen.getByPlaceholderText(translations.preCompletionNotes), - ).toHaveValue('task to be done with high priority'); - - fireEvent.change(screen.getByLabelText(translations.dueDate), { - target: { value: '04/05/2024' }, - }); - expect(screen.getByLabelText(translations.dueDate)).toHaveValue( - '04/05/2024', - ); - - userEvent.click(screen.getByTestId('createActionItemFormSubmitBtn')); - await wait(); - - expect(toast.error).toBeCalled(); - }); - - test('Raises an error when incorrect information is filled while updation', async () => { - window.location.assign('/event/111/123'); - render( - - - - - - {} - - - - - , - ); - await wait(); - const updateButtons = screen.getAllByTestId('editActionItemModalBtn'); - userEvent.click(updateButtons[0]); - - expect( - screen.getByText(translations.actionItemDetails), - ).toBeInTheDocument(); - - userEvent.click(screen.getByTestId('updateActionItemFormSubmitBtn')); - - await wait(); - - expect(toast.error).toBeCalled(); - }); - - test('Displays message when no data is available', async () => { - window.location.assign('/event/111/123'); - render( - - - - - - {} - - - - - , - ); - await wait(); - expect(screen.getByText('Nothing Found !!')).toBeInTheDocument(); - }); - - test('Testing update action modal to have correct initial values', async () => { - window.location.assign('/event/111/123'); - render( - - - - - - {} - - - - - , - ); - await wait(); - const updateButtons = screen.getAllByTestId('editActionItemModalBtn'); - userEvent.click(updateButtons[0]); - - expect(screen.getByText('Action Item Details')).toBeInTheDocument(); - const assigneeDropdown = screen.getByTestId( - 'formUpdateAssignee', - ) as HTMLSelectElement; - expect(assigneeDropdown.value).toBe('658930fd2caa9d8d6908745c'); - expect(assigneeDropdown).toHaveTextContent('Teresa Bradley'); - - expect( - screen.getByPlaceholderText(translations.preCompletionNotes), - ).toHaveValue('Pre Completion Notes'); - const editActionItem = screen.getByRole('button', { - name: translations.editActionItem, - }); - expect(editActionItem).toBeInTheDocument(); - }); -}); diff --git a/src/components/EventManagement/EventActionItems/EventActionItems.tsx b/src/components/EventManagement/EventActionItems/EventActionItems.tsx deleted file mode 100644 index 0f9d723adc..0000000000 --- a/src/components/EventManagement/EventActionItems/EventActionItems.tsx +++ /dev/null @@ -1,599 +0,0 @@ -import { useMutation, useQuery } from '@apollo/client'; -import type { Dayjs } from 'dayjs'; -import dayjs from 'dayjs'; -import type { ChangeEvent } from 'react'; -import React, { useEffect, useState } from 'react'; -import { Button, Form } from 'react-bootstrap'; -import { useTranslation } from 'react-i18next'; -import { toast } from 'react-toastify'; -import styles from './EventActionItems.module.css'; -import { DataGrid } from '@mui/x-data-grid'; -// import type { GridCellParams } from '@mui/x-data-grid'; -import { Stack } from '@mui/material'; -import Modal from 'react-bootstrap/Modal'; -import { - CREATE_ACTION_ITEM_MUTATION, - DELETE_ACTION_ITEM_MUTATION, - UPDATE_ACTION_ITEM_MUTATION, -} from 'GraphQl/Mutations/ActionItemMutations'; -import type { - InterfaceActionItemCategoryList, - InterfaceActionItemInfo, - InterfaceMembersList, -} from 'utils/interfaces'; -import { DatePicker } from '@mui/x-date-pickers'; -import { - ACTION_ITEM_CATEGORY_LIST, - MEMBERS_LIST, -} from 'GraphQl/Queries/Queries'; -import { ACTION_ITEM_LIST_BY_EVENTS } from 'GraphQl/Queries/ActionItemQueries'; -import { useEventActionColumnConfig } from './useEventActionColumnConfig'; -import ActionItemPreviewModal from 'screens/OrganizationActionItems/ActionItemPreviewModal'; -import ActionItemDeleteModal from 'screens/OrganizationActionItems/ActionItemDeleteModal'; - -function eventActionItems(props: { eventId: string }): JSX.Element { - const { eventId } = props; - const { t } = useTranslation('translation', { - keyPrefix: 'eventActionItems', - }); - const { t: tCommon } = useTranslation('common'); - - const [actionItemPreviewModalIsOpen, setActionItemPreviewModalIsOpen] = - useState(false); - const [actionItemStatusModal, setActionItemStatusModal] = useState(false); - const [isActionItemCompleted, setIsActionItemCompleted] = useState(false); - const [assignmentDate, setAssignmentDate] = useState(new Date()); - const [actionItemCreateModalIsOpen, setActionItemCreateModalIsOpen] = - useState(false); - const [actionItemUpdateModalIsOpen, setActionItemUpdateModalIsOpen] = - useState(false); - const [actionItemDeleteModalIsOpen, setActionItemDeleteModalIsOpen] = - useState(false); - const [dueDate, setDueDate] = useState(new Date()); - const [completionDate, setCompletionDate] = useState(new Date()); - const [actionItemId, setActionItemId] = useState(''); - document.title = t('title'); - const url: string = window.location.href; - const startIdx: number = url.indexOf('/event/') + '/event/'.length; - const orgId: string = url.slice(startIdx, url.indexOf('/', startIdx)); - const [formState, setFormState] = useState({ - actionItemCategoryId: '', - assignee: '', - assigner: '', - assigneeId: '', - preCompletionNotes: '', - postCompletionNotes: '', - isCompleted: false, - }); - const showCreateModal = (): void => { - const newState = !actionItemCreateModalIsOpen; - setActionItemCreateModalIsOpen(newState); - }; - const hideCreateModal = (): void => { - setActionItemCreateModalIsOpen(!actionItemCreateModalIsOpen); - }; - const showUpdateModal = (): void => { - setActionItemUpdateModalIsOpen(!actionItemUpdateModalIsOpen); - }; - const hideUpdateModal = (): void => { - setActionItemId(''); - setActionItemUpdateModalIsOpen(!actionItemUpdateModalIsOpen); - }; - const toggleDeleteModal = (): void => { - setActionItemDeleteModalIsOpen(!actionItemDeleteModalIsOpen); - }; - const setActionItemState = (actionItem: InterfaceActionItemInfo): void => { - setFormState((prevState) => ({ - ...prevState, - assignee: `${actionItem.assignee.firstName} ${actionItem.assignee.lastName}`, - assigner: `${actionItem.assigner.firstName} ${actionItem.assigner.lastName}`, - assigneeId: actionItem.assignee._id, - preCompletionNotes: actionItem.preCompletionNotes, - postCompletionNotes: actionItem.postCompletionNotes, - isCompleted: actionItem.isCompleted, - })); - setActionItemId(actionItem._id); - setDueDate(actionItem.dueDate); - setAssignmentDate(actionItem.assignmentDate); - setCompletionDate(actionItem.completionDate); - }; - const { - data: actionItemCategoriesData, - }: { - data: InterfaceActionItemCategoryList | undefined; - loading: boolean; - error?: Error | undefined; - } = useQuery(ACTION_ITEM_CATEGORY_LIST, { - variables: { - organizationId: orgId, - }, - }); - const actionItemCategories = - actionItemCategoriesData?.actionItemCategoriesByOrganization.filter( - (category) => !category.isDisabled, - ); - const { data: actionItemsData, refetch: actionItemsRefetch } = useQuery( - ACTION_ITEM_LIST_BY_EVENTS, - { - variables: { - eventId, - }, - }, - ); - const { - data: membersData, - }: { - data: InterfaceMembersList | undefined; - loading: boolean; - error?: Error | undefined; - } = useQuery(MEMBERS_LIST, { - variables: { id: orgId }, - }); - const [createActionItem] = useMutation(CREATE_ACTION_ITEM_MUTATION); - const createActionItemHandler = async ( - e: ChangeEvent, - ): Promise => { - e.preventDefault(); - try { - await createActionItem({ - variables: { - assigneeId: formState.assigneeId, - actionItemCategoryId: formState.actionItemCategoryId, - eventId, - preCompletionNotes: formState.preCompletionNotes, - dueDate: dayjs(dueDate).format('YYYY-MM-DD'), - }, - }); - setFormState({ - actionItemCategoryId: '', - assignee: '', - assigner: '', - assigneeId: '', - preCompletionNotes: '', - postCompletionNotes: '', - isCompleted: false, - }); - setDueDate(new Date()); - actionItemsRefetch(); - hideCreateModal(); - toast.success(t('successfulCreation') as string); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - } - } - }; - useEffect(() => { - actionItemsRefetch({ - eventId, - }); - }, []); - const [updateActionItem] = useMutation(UPDATE_ACTION_ITEM_MUTATION); - const updateActionItemHandler = async ( - e: ChangeEvent, - ): Promise => { - e.preventDefault(); - try { - await updateActionItem({ - variables: { - actionItemId, - assigneeId: formState.assigneeId, - preCompletionNotes: formState.preCompletionNotes, - postCompletionNotes: formState.postCompletionNotes, - dueDate: dayjs(dueDate).format('YYYY-MM-DD'), - completionDate: dayjs(completionDate).format('YYYY-MM-DD'), - isCompleted: formState.isCompleted, - }, - }); - actionItemsRefetch(); - hideUpdateModal(); - hideActionItemStatusModal(); - toast.success(t('successfulUpdation') as string); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - } - } - }; - const [removeActionItem] = useMutation(DELETE_ACTION_ITEM_MUTATION); - const deleteActionItemHandler = async (): Promise => { - try { - await removeActionItem({ - variables: { - actionItemId, - }, - }); - actionItemsRefetch(); - toggleDeleteModal(); - hidePreviewModal(); - toast.success(t('successfulDeletion') as string); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - console.log(error.message); - } - } - }; - - const handleActionItemStatusChange = ( - actionItem: InterfaceActionItemInfo, - ): void => { - actionItem = { ...actionItem, isCompleted: !actionItem.isCompleted }; - setIsActionItemCompleted(!actionItem.isCompleted); - setActionItemState(actionItem); - setActionItemStatusModal(true); - }; - - const showPreviewModal = (actionItem: InterfaceActionItemInfo): void => { - setActionItemState(actionItem); - setActionItemPreviewModalIsOpen(true); - }; - - const handleEditClick = (actionItem: InterfaceActionItemInfo): void => { - setActionItemId(actionItem._id); - setActionItemState(actionItem); - showUpdateModal(); - }; - - const hidePreviewModal = (): void => { - setActionItemPreviewModalIsOpen(false); - }; - - const hideActionItemStatusModal = (): void => { - setActionItemStatusModal(false); - setActionItemUpdateModalIsOpen(false); - }; - - const { columns } = useEventActionColumnConfig({ - eventId, - handleActionItemStatusChange, - showPreviewModal, - handleEditClick, - }); - - return ( - <> - -
- {/* create action item modal */} - - -

{t('actionItemDetails')}

- -
- -
- - {t('actionItemCategory')} - - setFormState({ - ...formState, - actionItemCategoryId: e.target.value, - }) - } - > - - {actionItemCategories?.map((category, index) => ( - - ))} - - - - {t('assignee')} - - setFormState({ ...formState, assigneeId: e.target.value }) - } - > - - {membersData?.organizations[0].members?.map((member, index) => ( - - ))} - - - - { - setFormState({ - ...formState, - preCompletionNotes: e.target.value, - }); - }} - /> -
- { - if (date) { - setDueDate(date?.toDate()); - } - }} - /> -
- - -
-
- {/* update action items modal */} - - -

{t('actionItemDetails')}

- -
- -
- - Assignee - - setFormState({ ...formState, assigneeId: e.target.value }) - } - > - - {membersData?.organizations[0].members.map((member, index) => { - const currMemberName = `${member.firstName} ${member.lastName}`; - if (currMemberName !== formState.assignee) { - return ( - - ); - } - })} - - - - { - setFormState({ - ...formState, - preCompletionNotes: e.target.value, - }); - }} - /> -
- { - if (date) { - setDueDate(date?.toDate()); - } - }} - /> -   - { - /* istanbul ignore next */ - if (date) { - setCompletionDate(date?.toDate()); - } - } - } - /> -
-
- -
- -
-
- - {/* preview modal */} - - - {/* Delete Modal */} - - - {/* action item status change modal */} - - -

{t('actionItemStatus')}

- -
- -
- - {isActionItemCompleted - ? t('preCompletionNotes') - : t('postCompletionNotes')} - - { - if (isActionItemCompleted) { - setFormState({ - ...formState, - preCompletionNotes: e.target.value, - }); - } else { - setFormState({ - ...formState, - postCompletionNotes: e.target.value, - }); - } - }} - /> - - -
-
- {actionItemsData && ( -
- row._id} - slots={{ - noRowsOverlay: () => ( - - Nothing Found !! - - ), - }} - sx={{ - '&.MuiDataGrid-root .MuiDataGrid-cell:focus-within': { - outline: 'none !important', - }, - '&.MuiDataGrid-root .MuiDataGrid-columnHeader:focus-within': { - outline: 'none', - }, - '& .MuiDataGrid-row:hover': { - backgroundColor: 'transparent', - }, - '& .MuiDataGrid-row.Mui-hovered': { - backgroundColor: 'transparent', - }, - '& .MuiDataGrid-columnHeaderTitle': { - fontWeight: 700, - }, - }} - getRowClassName={() => `${styles.rowBackground}`} - autoHeight - rowHeight={50} - columnHeaderHeight={40} - rows={actionItemsData?.actionItemsByEvent?.map( - (item: object, index: number) => ({ - ...item, - index: index + 1, - }), - )} - columns={columns} - isRowSelectable={() => false} - /> -
- )} - - ); -} -export default eventActionItems; diff --git a/src/components/EventManagement/EventActionItems/useEventActionColumnConfig.tsx b/src/components/EventManagement/EventActionItems/useEventActionColumnConfig.tsx deleted file mode 100644 index b1ec584f3b..0000000000 --- a/src/components/EventManagement/EventActionItems/useEventActionColumnConfig.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import React from 'react'; -import type { GridCellParams, GridColDef } from '@mui/x-data-grid'; -import { Link } from 'react-router-dom'; -import { Button, OverlayTrigger, Popover } from 'react-bootstrap'; -import styles from './EventActionItems.module.css'; -import type { InterfaceActionItemInfo } from 'utils/interfaces'; -import { useTranslation } from 'react-i18next'; - -export type Props = { - eventId: string; - handleActionItemStatusChange: (actionItem: InterfaceActionItemInfo) => void; - showPreviewModal: (actionItem: InterfaceActionItemInfo) => void; - handleEditClick: (actionItem: InterfaceActionItemInfo) => void; -}; - -type ColumnConfig = { - /** Configuration for the columns of the data grid. */ - columns: GridColDef[]; -}; - -const popover = ( - actionItemId: string, - actionItemNotes: string, -): JSX.Element => { - return ( - - {actionItemNotes} - - ); -}; - -export const useEventActionColumnConfig = ({ - eventId, - handleActionItemStatusChange, - showPreviewModal, - handleEditClick, -}: Props): ColumnConfig => { - const { t } = useTranslation('translation', { - keyPrefix: 'eventActionItems', - }); - const columns: GridColDef[] = [ - { - field: 'serialNo', - headerName: '#', - flex: 1, - minWidth: 50, - align: 'center', - headerAlign: 'center', - headerClassName: `${styles.tableHeader}`, - sortable: false, - renderCell: (params: GridCellParams) => { - return params.row?.index; - }, - }, - { - field: 'assignee', - headerName: 'Assignee', - flex: 2, - minWidth: 150, - align: 'center', - headerAlign: 'center', - headerClassName: `${styles.tableHeader}`, - sortable: false, - renderCell: (params: GridCellParams) => { - return ( - - {params.row?.assignee.firstName + - ' ' + - params.row?.assignee.lastName} - - ); - }, - }, - { - field: 'actionItemCategory', - headerName: 'Action Item Category', - flex: 2, - minWidth: 100, - align: 'center', - headerAlign: 'center', - headerClassName: `${styles.tableHeader}`, - sortable: false, - renderCell: (params: GridCellParams) => { - return params.row.actionItemCategory.name; - }, - }, - { - field: 'notes', - headerName: 'Notes', - minWidth: 150, - align: 'center', - headerAlign: 'center', - headerClassName: `${styles.tableHeader}`, - flex: 2, - sortable: false, - renderCell: (params: GridCellParams) => { - const actionItem = params.row; - return ( - - - {actionItem.preCompletionNotes.length > 25 - ? `${actionItem.preCompletionNotes.substring(0, 25)}...` - : actionItem.preCompletionNotes} - - - ); - }, - }, - { - field: 'completionNotes', - headerName: 'Completion Notes', - minWidth: 150, - align: 'center', - headerAlign: 'center', - headerClassName: `${styles.tableHeader}`, - flex: 2, - sortable: false, - renderCell: (params: GridCellParams) => { - const actionItem = params.row; - return actionItem.isCompleted ? ( - - - {actionItem.postCompletionNotes?.length > 25 - ? `${actionItem.postCompletionNotes.substring(0, 25)}...` - : actionItem.postCompletionNotes} - - - ) : ( - - {t('actionItemActive')} - - ); - }, - }, - { - field: 'options', - headerName: 'Options', - flex: 2, - minWidth: 100, - align: 'center', - headerAlign: 'center', - headerClassName: `${styles.tableHeader}`, - sortable: false, - renderCell: (params: GridCellParams) => { - return ( -
- handleActionItemStatusChange(params.row)} - /> - - -
- ); - }, - }, - ]; - return { - columns, - }; -}; diff --git a/src/components/IconComponent/IconComponent.test.tsx b/src/components/IconComponent/IconComponent.test.tsx index 4e31d9980d..686a8e6f75 100644 --- a/src/components/IconComponent/IconComponent.test.tsx +++ b/src/components/IconComponent/IconComponent.test.tsx @@ -35,10 +35,6 @@ const screenTestIdMap: Record> = { name: 'Action Items', testId: 'Icon-Component-ActionItemIcon', }, - AgendaItemsCategory: { - name: 'Agenda Items Category', - testId: 'Icon-Component-AgendaCategoryIcon', - }, Posts: { name: 'Posts', testId: 'Icon-Component-PostsIcon', @@ -63,10 +59,6 @@ const screenTestIdMap: Record> = { name: 'Check In Registrants', testId: 'Icon-Component-Check-In-Registrants', }, - EventStats: { - name: 'Event Stats', - testId: 'Icon-Component-Event-Stats', - }, Advertisement: { name: 'Advertisement', testId: 'Icon-Component-Advertisement', diff --git a/src/components/IconComponent/IconComponent.tsx b/src/components/IconComponent/IconComponent.tsx index 7aeeacfad4..ec058a2e80 100644 --- a/src/components/IconComponent/IconComponent.tsx +++ b/src/components/IconComponent/IconComponent.tsx @@ -4,11 +4,9 @@ import { NewspaperOutlined, } from '@mui/icons-material'; import { ReactComponent as ActionItemIcon } from 'assets/svgs/actionItem.svg'; -import { ReactComponent as AgendaCategoryIcon } from 'assets/svgs/agenda-category-icon.svg'; import { ReactComponent as BlockUserIcon } from 'assets/svgs/blockUser.svg'; import { ReactComponent as CheckInRegistrantsIcon } from 'assets/svgs/checkInRegistrants.svg'; import { ReactComponent as DashboardIcon } from 'assets/svgs/dashboard.svg'; -import { ReactComponent as EventStatsIcon } from 'assets/svgs/eventStats.svg'; import { ReactComponent as EventsIcon } from 'assets/svgs/events.svg'; import { ReactComponent as FundsIcon } from 'assets/svgs/funds.svg'; import { ReactComponent as ListEventRegistrantsIcon } from 'assets/svgs/listEventRegistrants.svg'; @@ -68,13 +66,6 @@ const iconComponent = (props: InterfaceIconComponent): JSX.Element => { data-testid="Icon-Component-ActionItemIcon" /> ); - case 'Agenda Items Category': - return ( - - ); case 'Posts': return ; case 'Block/Unblock': @@ -112,13 +103,6 @@ const iconComponent = (props: InterfaceIconComponent): JSX.Element => { stroke={props.fill} /> ); - case 'Event Stats': - return ( - - ); case 'Advertisement': return ( { ); await wait(); expect( - screen.getByText(/Error Occured while loading the Organization/i), + screen.getByText(/Error occured while loading Organization data/i), ).toBeInTheDocument(); }); diff --git a/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx b/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx index 7305abbaf5..b3f91aa7e8 100644 --- a/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx +++ b/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx @@ -37,7 +37,8 @@ const leftDrawerOrg = ({ setHideDrawer, }: InterfaceLeftDrawerProps): JSX.Element => { const { t: tCommon } = useTranslation('common'); - const [showDropdown, setShowDropdown] = React.useState(false); + const { t: tErrors } = useTranslation('errors'); + const [showDropdown, setShowDropdown] = useState(false); const [organization, setOrganization] = useState(); @@ -58,7 +59,6 @@ const leftDrawerOrg = ({ let isMounted = true; if (data && isMounted) { setOrganization(data?.organizations[0]); - console.log(targets, 'targets'); } return () => { isMounted = false; @@ -95,7 +95,7 @@ const leftDrawerOrg = ({ {/* Organization Section */} -
+
{loading ? ( <>
- Error Occured while loading the Organization + {tErrors('errorLoading', { entity: 'Organization' })} ) : ( @@ -123,6 +123,7 @@ const leftDrawerOrg = ({ ) : ( )} @@ -139,10 +140,10 @@ const leftDrawerOrg = ({
{/* Options List */} +
+ {tCommon('menu')} +
-
- {tCommon('menu')} -
{targets.map(({ name, url }, index) => { return url ? ( diff --git a/src/components/OrgActionItemCategories/OrgActionItemCategories.module.css b/src/components/OrgActionItemCategories/OrgActionItemCategories.module.css deleted file mode 100644 index ac9f4a5900..0000000000 --- a/src/components/OrgActionItemCategories/OrgActionItemCategories.module.css +++ /dev/null @@ -1,33 +0,0 @@ -.addButton { - width: 7em; - position: absolute; - right: 1rem; - top: 1rem; -} - -.createModal { - margin-top: 20vh; - margin-left: 13vw; - max-width: 80vw; -} - -.icon { - transform: scale(1.5); - color: var(--bs-danger); - margin-bottom: 1rem; -} - -.message { - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -} - -.titlemodal { - color: var(--bs-gray-600); - font-weight: 600; - font-size: 20px; - margin-top: 1rem; - width: 65%; -} diff --git a/src/components/OrgActionItemCategories/OrgActionItemCategories.test.tsx b/src/components/OrgActionItemCategories/OrgActionItemCategories.test.tsx deleted file mode 100644 index 0665ce6fc4..0000000000 --- a/src/components/OrgActionItemCategories/OrgActionItemCategories.test.tsx +++ /dev/null @@ -1,372 +0,0 @@ -import React from 'react'; -import { - render, - screen, - fireEvent, - waitFor, - act, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import 'jest-localstorage-mock'; -import { MockedProvider } from '@apollo/client/testing'; -import 'jest-location-mock'; -import { I18nextProvider } from 'react-i18next'; -import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; -import i18n from 'utils/i18nForTest'; -import { toast } from 'react-toastify'; - -import { store } from 'state/store'; -import { StaticMockLink } from 'utils/StaticMockLink'; - -import OrgActionItemCategories from './OrgActionItemCategories'; -import { - MOCKS, - MOCKS_ERROR_QUERY, - MOCKS_ERROR_MUTATIONS, -} from './OrgActionItemCategoryMocks'; - -jest.mock('react-toastify', () => ({ - toast: { - success: jest.fn(), - error: jest.fn(), - }, -})); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: '123' }), -})); - -async function wait(ms = 100): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); -} - -const link = new StaticMockLink(MOCKS, true); -const link2 = new StaticMockLink(MOCKS_ERROR_QUERY, true); -const link3 = new StaticMockLink(MOCKS_ERROR_MUTATIONS, true); - -const translations = { - ...JSON.parse( - JSON.stringify( - i18n.getDataByLanguage('en')?.translation.orgActionItemCategories ?? {}, - ), - ), - ...JSON.parse(JSON.stringify(i18n.getDataByLanguage('en')?.common ?? {})), - ...JSON.parse(JSON.stringify(i18n.getDataByLanguage('en')?.errors ?? {})), -}; - -describe('Testing Action Item Categories Component', () => { - test('Component loads correctly', async () => { - window.location.assign('/orgsetting/123'); - const { getByText } = render( - - - - - {} - - - - , - ); - - await wait(); - - await waitFor(() => { - expect(getByText(translations.create)).toBeInTheDocument(); - }); - }); - - test('render error component on unsuccessful query', async () => { - window.location.assign('/orgsetting/123'); - const { queryByText } = render( - - - - - {} - - - - , - ); - - await wait(); - - await waitFor(() => { - expect(queryByText(translations.create)).not.toBeInTheDocument(); - }); - }); - - test('opens and closes create and update modals on button clicks', async () => { - window.location.assign('/orgsetting/123'); - render( - - - - - {} - - - - , - ); - - await wait(); - - await waitFor(() => { - userEvent.click(screen.getByTestId('actionItemCategoryModalOpenBtn')); - userEvent.click(screen.getByTestId('actionItemCategoryModalCloseBtn')); - }); - - await waitFor(() => { - userEvent.click( - screen.getAllByTestId('actionItemCategoryUpdateModalOpenBtn')[0], - ); - userEvent.click(screen.getByTestId('actionItemCategoryModalCloseBtn')); - }); - }); - - test('create a new action item category', async () => { - window.location.assign('/orgsetting/123'); - render( - - - - - {} - - - - , - ); - - await wait(); - - await waitFor(() => { - userEvent.click(screen.getByTestId('actionItemCategoryModalOpenBtn')); - userEvent.type( - screen.getByPlaceholderText(translations.enterName), - 'ActionItemCategory 4', - ); - - userEvent.click(screen.getByTestId('formSubmitButton')); - }); - - await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.successfulCreation); - }); - }); - - test('toast error on unsuccessful creation', async () => { - window.location.assign('/orgsetting/123'); - render( - - - - - {} - - - - , - ); - - await wait(); - - await waitFor(() => { - userEvent.click(screen.getByTestId('actionItemCategoryModalOpenBtn')); - userEvent.type( - screen.getByPlaceholderText(translations.enterName), - 'ActionItemCategory 4', - ); - - userEvent.click(screen.getByTestId('formSubmitButton')); - }); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); - }); - }); - - test('update an action item category', async () => { - window.location.assign('/orgsetting/123'); - render( - - - - - {} - - - - , - ); - - await wait(); - - await waitFor(() => { - userEvent.click( - screen.getAllByTestId('actionItemCategoryUpdateModalOpenBtn')[0], - ); - - const name = screen.getByPlaceholderText(translations.enterName); - fireEvent.change(name, { target: { value: '' } }); - - userEvent.type( - screen.getByPlaceholderText(translations.enterName), - 'ActionItemCategory 1 updated', - ); - - userEvent.click(screen.getByTestId('formSubmitButton')); - }); - - await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.successfulUpdation); - }); - }); - - test('toast error on unsuccessful updation', async () => { - window.location.assign('/orgsetting/123'); - render( - - - - - {} - - - - , - ); - - await wait(); - - await waitFor(() => { - userEvent.click( - screen.getAllByTestId('actionItemCategoryUpdateModalOpenBtn')[0], - ); - - const name = screen.getByPlaceholderText(translations.enterName); - fireEvent.change(name, { target: { value: '' } }); - - userEvent.type( - screen.getByPlaceholderText(translations.enterName), - 'ActionItemCategory 1 updated', - ); - - userEvent.click(screen.getByTestId('formSubmitButton')); - }); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); - }); - }); - - test('toast error on providing the same name on updation', async () => { - window.location.assign('/orgsetting/123'); - render( - - - - - {} - - - - , - ); - - await wait(); - - await waitFor(() => { - userEvent.click( - screen.getAllByTestId('actionItemCategoryUpdateModalOpenBtn')[0], - ); - - const name = screen.getByPlaceholderText(translations.enterName); - fireEvent.change(name, { target: { value: '' } }); - - userEvent.type( - screen.getByPlaceholderText(translations.enterName), - 'ActionItemCategory 1', - ); - - userEvent.click(screen.getByTestId('formSubmitButton')); - }); - - await waitFor(() => { - expect(toast.error).toBeCalledWith(translations.sameNameConflict); - }); - }); - - test('toggle the disablity status of an action item category', async () => { - window.location.assign('/orgsetting/123'); - render( - - - - - {} - - - - , - ); - - await wait(); - - await waitFor(() => { - userEvent.click(screen.getAllByTestId('disabilityStatusButton')[0]); - }); - - await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.categoryDisabled); - }); - - await waitFor(() => { - userEvent.click(screen.getAllByTestId('disabilityStatusButton')[1]); - }); - - await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.categoryEnabled); - }); - }); - - test('toast error on unsuccessful toggling of the disablity status', async () => { - window.location.assign('/orgsetting/123'); - render( - - - - - {} - - - - , - ); - - await wait(); - - await waitFor(() => { - userEvent.click(screen.getAllByTestId('disabilityStatusButton')[0]); - }); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); - }); - - await waitFor(() => { - userEvent.click(screen.getAllByTestId('disabilityStatusButton')[1]); - }); - - await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); - }); - }); -}); diff --git a/src/components/OrgActionItemCategories/OrgActionItemCategories.tsx b/src/components/OrgActionItemCategories/OrgActionItemCategories.tsx deleted file mode 100644 index ea86fcf965..0000000000 --- a/src/components/OrgActionItemCategories/OrgActionItemCategories.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import type { ChangeEvent } from 'react'; -import React, { useState } from 'react'; -import { Button, Form, Modal } from 'react-bootstrap'; -import styles from './OrgActionItemCategories.module.css'; -import { useTranslation } from 'react-i18next'; -import { toast } from 'react-toastify'; -import { WarningAmberRounded } from '@mui/icons-material'; - -import { useMutation, useQuery } from '@apollo/client'; -import { - CREATE_ACTION_ITEM_CATEGORY_MUTATION, - UPDATE_ACTION_ITEM_CATEGORY_MUTATION, -} from 'GraphQl/Mutations/mutations'; -import { ACTION_ITEM_CATEGORY_LIST } from 'GraphQl/Queries/Queries'; -import type { InterfaceActionItemCategoryList } from 'utils/interfaces'; -import Loader from 'components/Loader/Loader'; -import { useParams } from 'react-router-dom'; - -type ModalType = 'Create' | 'Update'; - -/** - * Represents the component for managing organization action item categories. - * This component allows creating, updating, enabling, and disabling action item categories. - */ -const OrgActionItemCategories = (): JSX.Element => { - const { t } = useTranslation('translation', { - keyPrefix: 'orgActionItemCategories', - }); - const { t: tCommon } = useTranslation('common'); - - // State variables - const [modalIsOpen, setModalIsOpen] = useState(false); // Controls modal visibility - const [modalType, setModalType] = useState('Create'); // Type of modal (Create or Update) - const [categoryId, setCategoryId] = useState(''); // Current category ID for updating - const [name, setName] = useState(''); // Category name for creation or update - const [currName, setCurrName] = useState(''); // Current category name (used for comparison) - - // Fetch organization ID from URL params - const { orgId: currentUrl } = useParams(); - - // Query to fetch action item categories - const { - data, - loading, - error, - refetch, - }: { - data: InterfaceActionItemCategoryList | undefined; - loading: boolean; - error?: Error | undefined; - refetch: () => void; - } = useQuery(ACTION_ITEM_CATEGORY_LIST, { - variables: { - organizationId: currentUrl, - }, - notifyOnNetworkStatusChange: true, - }); - - // Mutations for creating and updating categories - const [createActionItemCategory] = useMutation( - CREATE_ACTION_ITEM_CATEGORY_MUTATION, - ); - - const [updateActionItemCategory] = useMutation( - UPDATE_ACTION_ITEM_CATEGORY_MUTATION, - ); - - // Handles category creation - const handleCreate = async ( - e: ChangeEvent, - ): Promise => { - e.preventDefault(); - try { - await createActionItemCategory({ - variables: { - name, - organizationId: currentUrl, - }, - }); - - setName(''); - refetch(); - - setModalIsOpen(false); - - toast.success(t('successfulCreation') as string); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - console.log(error.message); - } - } - }; - - // Handles category update - const handleEdit = async (e: ChangeEvent): Promise => { - e.preventDefault(); - if (name === currName) { - toast.error(t('sameNameConflict') as string); // Show error if the name is the same - } else { - try { - await updateActionItemCategory({ - variables: { - actionItemCategoryId: categoryId, - name, - }, - }); - - setName(''); // Clear the name input - setCategoryId(''); // Clear the category ID - refetch(); // Refetch the list of categories - setModalIsOpen(false); // Close the modal - - toast.success(t('successfulUpdation') as string); // Show success toast - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); // Show error toast - console.log(error.message); // Log the error - } - } - } - }; - - // Handles enabling or disabling a category - const handleStatusChange = async ( - id: string, - disabledStatus: boolean, - ): Promise => { - try { - await updateActionItemCategory({ - variables: { - actionItemCategoryId: id, - isDisabled: !disabledStatus, - }, - }); - - refetch(); // Refetch the list of categories - - toast.success( - disabledStatus - ? (t('categoryEnabled') as string) - : (t('categoryDisabled') as string), - ); // Show success toast - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - console.log(error.message); - } - } - }; - - // Shows the modal for creating a new category - const showCreateModal = (): void => { - setModalType('Create'); - setModalIsOpen(true); - }; - - // Shows the modal for updating an existing category - const showUpdateModal = (name: string, id: string): void => { - setCurrName(name); - setName(name); - setCategoryId(id); - setModalType('Update'); - setModalIsOpen(true); - }; - - // Hides the modal and clears input fields - const hideModal = (): void => { - setName(''); - setCategoryId(''); - setModalIsOpen(false); - }; - - // Show loader while data is being fetched - if (loading) { - return ; - } - - // Show error message if there's an error - if (error) { - return ( -
- -
- Error occured while loading Action Item Categories Data -
- {`${error.message}`} -
-
- ); - } - - // Render the list of action item categories - const actionItemCategories = data?.actionItemCategoriesByOrganization; - - return ( - <> - - -
- {actionItemCategories?.map((category, index) => { - return ( -
-
-
- {category.name} -
-
- - -
-
- - {index !== actionItemCategories.length - 1 &&
} -
- ); - })} -
- - {/* Modal for creating or updating categories */} - - -

- {t('actionItemCategoryDetails')} -

- -
- -
- - {t('actionItemCategoryName')} - - { - setName(e.target.value); - }} - /> - - -
-
- - ); -}; - -export default OrgActionItemCategories; diff --git a/src/components/OrgActionItemCategories/OrgActionItemCategoryMocks.ts b/src/components/OrgActionItemCategories/OrgActionItemCategoryMocks.ts deleted file mode 100644 index b03e53c5f5..0000000000 --- a/src/components/OrgActionItemCategories/OrgActionItemCategoryMocks.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { - CREATE_ACTION_ITEM_CATEGORY_MUTATION, - UPDATE_ACTION_ITEM_CATEGORY_MUTATION, -} from 'GraphQl/Mutations/mutations'; - -import { ACTION_ITEM_CATEGORY_LIST } from 'GraphQl/Queries/Queries'; - -export const MOCKS = [ - { - request: { - query: ACTION_ITEM_CATEGORY_LIST, - variables: { organizationId: '123' }, - }, - result: { - data: { - actionItemCategoriesByOrganization: [ - { - _id: '1', - name: 'ActionItemCategory 1', - isDisabled: false, - }, - { - _id: '2', - name: 'ActionItemCategory 2', - isDisabled: true, - }, - { - _id: '3', - name: 'ActionItemCategory 3', - isDisabled: false, - }, - ], - }, - }, - }, - { - request: { - query: CREATE_ACTION_ITEM_CATEGORY_MUTATION, - variables: { name: 'ActionItemCategory 4', organizationId: '123' }, - }, - result: { - data: { - createActionItemCategory: { - _id: '4', - }, - }, - }, - }, - { - request: { - query: UPDATE_ACTION_ITEM_CATEGORY_MUTATION, - variables: { - name: 'ActionItemCategory 1 updated', - actionItemCategoryId: '1', - }, - }, - result: { - data: { - updateActionItemCategory: { - _id: '1', - }, - }, - }, - }, - { - request: { - query: UPDATE_ACTION_ITEM_CATEGORY_MUTATION, - variables: { - isDisabled: true, - actionItemCategoryId: '1', - }, - }, - result: { - data: { - updateActionItemCategory: { - _id: '1', - }, - }, - }, - }, - { - request: { - query: UPDATE_ACTION_ITEM_CATEGORY_MUTATION, - variables: { - isDisabled: false, - actionItemCategoryId: '2', - }, - }, - result: { - data: { - updateActionItemCategory: { - _id: '2', - }, - }, - }, - }, -]; - -export const MOCKS_ERROR_QUERY = [ - { - request: { - query: ACTION_ITEM_CATEGORY_LIST, - variables: { organizationId: '123' }, - }, - error: new Error('Mock Graphql Error'), - }, -]; - -export const MOCKS_ERROR_MUTATIONS = [ - { - request: { - query: ACTION_ITEM_CATEGORY_LIST, - variables: { organizationId: '123' }, - }, - result: { - data: { - actionItemCategoriesByOrganization: [ - { - _id: '1', - name: 'ActionItemCategory 1', - isDisabled: false, - }, - { - _id: '2', - name: 'ActionItemCategory 2', - isDisabled: true, - }, - { - _id: '3', - name: 'ActionItemCategory 3', - isDisabled: false, - }, - ], - }, - }, - }, - { - request: { - query: CREATE_ACTION_ITEM_CATEGORY_MUTATION, - variables: { name: 'ActionItemCategory 4', organizationId: '123' }, - }, - error: new Error('Mock Graphql Error'), - }, - { - request: { - query: UPDATE_ACTION_ITEM_CATEGORY_MUTATION, - variables: { - name: 'ActionItemCategory 1 updated', - actionItemCategoryId: '1', - }, - }, - error: new Error('Mock Graphql Error'), - }, - { - request: { - query: UPDATE_ACTION_ITEM_CATEGORY_MUTATION, - variables: { - isDisabled: true, - actionItemCategoryId: '1', - }, - }, - error: new Error('Mock Graphql Error'), - }, - { - request: { - query: UPDATE_ACTION_ITEM_CATEGORY_MUTATION, - variables: { - isDisabled: false, - actionItemCategoryId: '2', - }, - }, - error: new Error('Mock Graphql Error'), - }, -]; diff --git a/src/components/OrgSettings/ActionItemCategories/CategoryModal.test.tsx b/src/components/OrgSettings/ActionItemCategories/CategoryModal.test.tsx new file mode 100644 index 0000000000..39d4884e8b --- /dev/null +++ b/src/components/OrgSettings/ActionItemCategories/CategoryModal.test.tsx @@ -0,0 +1,208 @@ +import React from 'react'; +import { MockedProvider } from '@apollo/react-testing'; +import type { RenderResult } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import { store } from 'state/store'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import i18n from 'utils/i18nForTest'; +import type { ApolloLink } from '@apollo/client'; +import { MOCKS, MOCKS_ERROR } from './OrgActionItemCategoryMocks'; +import type { InterfaceActionItemCategoryModal } from './CategoryModal'; +import CategoryModal from './CategoryModal'; +import { toast } from 'react-toastify'; + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +const link1 = new StaticMockLink(MOCKS); +const link3 = new StaticMockLink(MOCKS_ERROR); +const translations = { + ...JSON.parse( + JSON.stringify( + i18n.getDataByLanguage('en')?.translation.orgActionItemCategories ?? {}, + ), + ), + ...JSON.parse(JSON.stringify(i18n.getDataByLanguage('en')?.common ?? {})), + ...JSON.parse(JSON.stringify(i18n.getDataByLanguage('en')?.errors ?? {})), +}; + +const categoryProps: InterfaceActionItemCategoryModal[] = [ + { + isOpen: true, + hide: jest.fn(), + refetchCategories: jest.fn(), + orgId: 'orgId', + mode: 'create', + category: { + _id: 'categoryId', + name: 'Category 1', + isDisabled: false, + createdAt: '2044-01-01', + creator: { _id: 'userId', firstName: 'John', lastName: 'Doe' }, + }, + }, + { + isOpen: true, + hide: jest.fn(), + refetchCategories: jest.fn(), + orgId: 'orgId', + mode: 'edit', + category: { + _id: 'categoryId', + name: 'Category 1', + isDisabled: false, + createdAt: '2044-01-01', + creator: { _id: 'userId', firstName: 'John', lastName: 'Doe' }, + }, + }, +]; + +const renderCategoryModal = ( + link: ApolloLink, + props: InterfaceActionItemCategoryModal, +): RenderResult => { + return render( + + + + + + + + + , + ); +}; + +const fillFormAndSubmit = async ( + name: string, + isDisabled: boolean, +): Promise => { + const nameInput = screen.getByLabelText('Name *'); + const isDisabledSwitch = screen.getByTestId('isDisabledSwitch'); + const submitBtn = screen.getByTestId('formSubmitButton'); + + fireEvent.change(nameInput, { target: { value: name } }); + if (isDisabled) { + userEvent.click(isDisabledSwitch); + } + userEvent.click(submitBtn); +}; + +describe('Testing Action Item Category Modal', () => { + it('should populate form fields with correct values in edit mode', async () => { + renderCategoryModal(link1, categoryProps[1]); + await waitFor(() => + expect( + screen.getByText(translations.categoryDetails), + ).toBeInTheDocument(), + ); + + expect(screen.getByLabelText('Name *')).toHaveValue('Category 1'); + expect(screen.getByTestId('isDisabledSwitch')).not.toBeChecked(); + }); + + it('should update name when input value changes', async () => { + renderCategoryModal(link1, categoryProps[1]); + const nameInput = screen.getByLabelText('Name *'); + expect(nameInput).toHaveValue('Category 1'); + fireEvent.change(nameInput, { target: { value: 'Category 2' } }); + expect(nameInput).toHaveValue('Category 2'); + }); + + it('should update isDisabled when switch is toggled', async () => { + renderCategoryModal(link1, categoryProps[1]); + const isDisabledSwitch = screen.getByTestId('isDisabledSwitch'); + expect(isDisabledSwitch).not.toBeChecked(); + userEvent.click(isDisabledSwitch); + expect(isDisabledSwitch).toBeChecked(); + }); + + it('should edit category', async () => { + renderCategoryModal(link1, categoryProps[1]); + await fillFormAndSubmit('Category 2', true); + + await waitFor(() => { + expect(categoryProps[1].refetchCategories).toHaveBeenCalled(); + expect(categoryProps[1].hide).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith( + translations.successfulUpdation, + ); + }); + }); + + it('Edit only Name', async () => { + renderCategoryModal(link1, categoryProps[1]); + await fillFormAndSubmit('Category 2', false); + + await waitFor(() => { + expect(categoryProps[1].refetchCategories).toHaveBeenCalled(); + expect(categoryProps[1].hide).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith( + translations.successfulUpdation, + ); + }); + }); + + it('Edit only isDisabled', async () => { + renderCategoryModal(link1, categoryProps[1]); + await fillFormAndSubmit('Category 1', true); + + await waitFor(() => { + expect(categoryProps[1].refetchCategories).toHaveBeenCalled(); + expect(categoryProps[1].hide).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith( + translations.successfulUpdation, + ); + }); + }); + + it('Error in updating category', async () => { + renderCategoryModal(link3, categoryProps[1]); + await fillFormAndSubmit('Category 2', true); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('Mock Graphql Error'); + }); + }); + + it('should create category', async () => { + renderCategoryModal(link1, categoryProps[0]); + await fillFormAndSubmit('Category 2', true); + + await waitFor(() => { + expect(categoryProps[0].refetchCategories).toHaveBeenCalled(); + expect(categoryProps[0].hide).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith( + translations.successfulCreation, + ); + }); + }); + + it('Error in creating category', async () => { + renderCategoryModal(link3, categoryProps[0]); + await fillFormAndSubmit('Category 2', true); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('Mock Graphql Error'); + }); + }); + + it('Try to edit without changing any field', async () => { + renderCategoryModal(link1, categoryProps[1]); + const submitBtn = screen.getByTestId('formSubmitButton'); + userEvent.click(submitBtn); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith(translations.sameNameConflict); + }); + }); +}); diff --git a/src/components/OrgSettings/ActionItemCategories/CategoryModal.tsx b/src/components/OrgSettings/ActionItemCategories/CategoryModal.tsx new file mode 100644 index 0000000000..43018db0ab --- /dev/null +++ b/src/components/OrgSettings/ActionItemCategories/CategoryModal.tsx @@ -0,0 +1,208 @@ +import React, { type ChangeEvent, type FC, useEffect, useState } from 'react'; +import { Button, Form, Modal } from 'react-bootstrap'; +import styles from './OrgActionItemCategories.module.css'; +import { useTranslation } from 'react-i18next'; +import type { InterfaceActionItemCategoryInfo } from 'utils/interfaces'; +import { useMutation } from '@apollo/client'; +import { + CREATE_ACTION_ITEM_CATEGORY_MUTATION, + UPDATE_ACTION_ITEM_CATEGORY_MUTATION, +} from 'GraphQl/Mutations/ActionItemCategoryMutations'; +import { toast } from 'react-toastify'; +import { FormControl, TextField } from '@mui/material'; + +/** + * Props for the `CategoryModal` component. + * + * + * isOpen - The state of the modal. + * hide - The function to hide the modal. + * refetchCategories - The function to refetch the categories. + * orgId - The organization ID. + * category - The category to be edited. + * mode - The mode of the modal. + * @returns The `CategoryModal` component. + */ +export interface InterfaceActionItemCategoryModal { + isOpen: boolean; + hide: () => void; + refetchCategories: () => void; + orgId: string; + category: InterfaceActionItemCategoryInfo | null; + mode: 'create' | 'edit'; +} + +/** + * A modal component for creating and editing action item categories. + * + * @param props - The properties passed to the component. + * @returns The `CategoryModal` component. + */ +const CategoryModal: FC = ({ + category, + hide, + isOpen, + mode, + refetchCategories, + orgId, +}) => { + const { t: tCommon } = useTranslation('common'); + const { t } = useTranslation('translation', { + keyPrefix: 'orgActionItemCategories', + }); + + const [formState, setFormState] = useState({ + name: category?.name ?? '', + isDisabled: category?.isDisabled ?? false, + }); + + const { name, isDisabled } = formState; + + useEffect(() => { + setFormState({ + name: category?.name ?? '', + isDisabled: category?.isDisabled ?? false, + }); + }, [category]); + + // Mutations for creating and updating categories + const [createActionItemCategory] = useMutation( + CREATE_ACTION_ITEM_CATEGORY_MUTATION, + ); + + const [updateActionItemCategory] = useMutation( + UPDATE_ACTION_ITEM_CATEGORY_MUTATION, + ); + + /** + * Handles category creation. + * + * @param e - The form submission event. + */ + const handleCreate = async ( + e: ChangeEvent, + ): Promise => { + e.preventDefault(); + try { + await createActionItemCategory({ + variables: { + name, + isDisabled, + organizationId: orgId, + }, + }); + + refetchCategories(); + hide(); + toast.success(t('successfulCreation')); + } catch (error: unknown) { + toast.error((error as Error).message); + } + }; + + /** + * Handles category update. + * + * @param e - The form submission event. + */ + const handleEdit = async (e: ChangeEvent): Promise => { + e.preventDefault(); + if (name === category?.name && isDisabled === category?.isDisabled) { + toast.error(t('sameNameConflict')); // Show error if the name is the same + } else { + try { + const updatedFields: { [key: string]: string | boolean } = {}; + if (name != category?.name) { + updatedFields.name = name; + } + if (isDisabled != category?.isDisabled) { + updatedFields.isDisabled = isDisabled; + } + + await updateActionItemCategory({ + variables: { + actionItemCategoryId: category?._id, + ...updatedFields, + }, + }); + + setFormState({ + name: '', + isDisabled: false, + }); + refetchCategories(); + hide(); + toast.success(t('successfulUpdation')); + } catch (error: unknown) { + toast.error((error as Error).message); + } + } + }; + + return ( + + +

{t('categoryDetails')}

+ +
+ +
+ {/* Input field to enter amount to be pledged */} + + + + setFormState({ ...formState, name: e.target.value }) + } + required + /> + + + + + setFormState({ + ...formState, + isDisabled: !isDisabled, + }) + } + /> + + + +
+
+
+ ); +}; + +export default CategoryModal; diff --git a/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.module.css b/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.module.css new file mode 100644 index 0000000000..919421b0f2 --- /dev/null +++ b/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.module.css @@ -0,0 +1,138 @@ +/* Button Styles */ +.addButton { + /* Position and size of the button */ + width: 7em; + position: absolute; + right: 1rem; + top: 1rem; +} + +/* Modal Styles */ +.createModal { + /* Position and size of the modal */ + margin-top: 20vh; + margin-left: 13vw; + max-width: 80vw; +} + +.icon { + /* Size and color of the icon */ + transform: scale(1.5); + color: var(--bs-danger); + margin-bottom: 1rem; +} + +.message { + /* Centering the content of the modal */ + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.titlemodal { + /* Styling for the modal title */ + color: #707070; + font-weight: 600; + font-size: 32px; + width: 65%; + margin-bottom: 0px; +} + +.modalCloseBtn { + /* Styling for the modal close button */ + width: 40px; + height: 40px; + padding: 1rem; + display: flex; + justify-content: center; + align-items: center; +} + +/* Input Styles */ +.noOutline input { + /* Removing the outline from the input */ + outline: none; +} + +/* Header and Action Item Categories Styles */ +.btnsContainer { + /* Styling for the container of the buttons */ + display: flex; + margin: 0.5rem 0 1.5rem 0; +} + +.btnsContainer .input { + /* Styling for the input field */ + flex: 1; + min-width: 18rem; + position: relative; +} + +.btnsContainer input { + /* Styling for the input border */ + outline: 1px solid var(--bs-gray-400); +} + +.btnsContainer .input button { + /* Styling for the button in the input field */ + width: 52px; +} + +.inputField { + margin-top: 10px; + margin-bottom: 10px; + background-color: white; + box-shadow: 0 1px 1px #31bb6b; +} + +.inputField > button { + padding-top: 10px; + padding-bottom: 10px; +} + +/* Dropdown Styles */ +.dropdown { + /* Styling for the dropdown */ + background-color: white; + border: 1px solid #31bb6b; + position: relative; + display: inline-block; + color: #31bb6b; +} + +/* Datagrid Styles */ +.rowBackground { + /* Styling for the row background */ + background-color: var(--bs-white); + max-height: 120px; +} + +.tableHeader { + /* Styling for the table header */ + background-color: var(--bs-primary); + color: var(--bs-white); + font-size: 1rem; +} + +.chipIcon { + /* Styling for the chip icon */ + height: 0.9rem !important; +} + +.chip { + /* Styling for the chip */ + height: 1.5rem !important; +} + +.active { + /* Styling for the active state */ + background-color: #31bb6a50 !important; +} + +.pending { + /* Styling for the pending state */ + background-color: #ffd76950 !important; + color: #bb952bd0 !important; + border-color: #bb952bd0 !important; +} diff --git a/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.test.tsx b/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.test.tsx new file mode 100644 index 0000000000..d3698bf346 --- /dev/null +++ b/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.test.tsx @@ -0,0 +1,241 @@ +import React from 'react'; +import { MockedProvider } from '@apollo/react-testing'; +import type { RenderResult } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import { store } from 'state/store'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import i18n from 'utils/i18nForTest'; +import type { ApolloLink } from '@apollo/client'; +import { MOCKS, MOCKS_EMPTY, MOCKS_ERROR } from './OrgActionItemCategoryMocks'; +import OrgActionItemCategories from './OrgActionItemCategories'; + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +jest.mock('@mui/x-date-pickers/DateTimePicker', () => { + return { + DateTimePicker: jest.requireActual( + '@mui/x-date-pickers/DesktopDateTimePicker', + ).DesktopDateTimePicker, + }; +}); + +const link1 = new StaticMockLink(MOCKS); +const link2 = new StaticMockLink(MOCKS_EMPTY); +const link3 = new StaticMockLink(MOCKS_ERROR); +const t = { + ...JSON.parse( + JSON.stringify( + i18n.getDataByLanguage('en')?.translation.orgActionItemCategories ?? {}, + ), + ), + ...JSON.parse(JSON.stringify(i18n.getDataByLanguage('en')?.common ?? {})), + ...JSON.parse(JSON.stringify(i18n.getDataByLanguage('en')?.errors ?? {})), +}; + +const renderActionItemCategories = ( + link: ApolloLink, + orgId: string, +): RenderResult => { + return render( + + + + + + + + + , + ); +}; + +describe('Testing Organisation Action Item Categories', () => { + it('should render the Action Item Categories Screen', async () => { + renderActionItemCategories(link1, 'orgId'); + await waitFor(() => { + expect(screen.getByTestId('searchByName')).toBeInTheDocument(); + expect(screen.getByText('Category 1')).toBeInTheDocument(); + expect(screen.getByText('Category 2')).toBeInTheDocument(); + }); + }); + + it('Sort the Categories (asc/desc) by createdAt', async () => { + renderActionItemCategories(link1, 'orgId'); + + const sortBtn = await screen.findByTestId('sort'); + expect(sortBtn).toBeInTheDocument(); + + // Sort by createdAt_DESC + fireEvent.click(sortBtn); + await waitFor(() => { + expect(screen.getByTestId('createdAt_DESC')).toBeInTheDocument(); + }); + fireEvent.click(screen.getByTestId('createdAt_DESC')); + await waitFor(() => { + expect(screen.getAllByTestId('categoryName')[0]).toHaveTextContent( + 'Category 1', + ); + }); + + // Sort by createdAt_ASC + fireEvent.click(sortBtn); + await waitFor(() => { + expect(screen.getByTestId('createdAt_ASC')).toBeInTheDocument(); + }); + fireEvent.click(screen.getByTestId('createdAt_ASC')); + await waitFor(() => { + expect(screen.getAllByTestId('categoryName')[0]).toHaveTextContent( + 'Category 2', + ); + }); + }); + + it('Filter the categories by status (All/Disabled)', async () => { + renderActionItemCategories(link1, 'orgId'); + + const filterBtn = await screen.findByTestId('filter'); + expect(filterBtn).toBeInTheDocument(); + + // Filter by All + fireEvent.click(filterBtn); + await waitFor(() => { + expect(screen.getByTestId('statusAll')).toBeInTheDocument(); + }); + fireEvent.click(screen.getByTestId('statusAll')); + + await waitFor(() => { + expect(screen.getByText('Category 1')).toBeInTheDocument(); + expect(screen.getByText('Category 2')).toBeInTheDocument(); + }); + + // Filter by Disabled + fireEvent.click(filterBtn); + await waitFor(() => { + expect(screen.getByTestId('statusDisabled')).toBeInTheDocument(); + }); + fireEvent.click(screen.getByTestId('statusDisabled')); + await waitFor(() => { + expect(screen.queryByText('Category 1')).toBeNull(); + expect(screen.getByText('Category 2')).toBeInTheDocument(); + }); + }); + + it('Filter the categories by status (Active)', async () => { + renderActionItemCategories(link1, 'orgId'); + + const filterBtn = await screen.findByTestId('filter'); + expect(filterBtn).toBeInTheDocument(); + + fireEvent.click(filterBtn); + await waitFor(() => { + expect(screen.getByTestId('statusActive')).toBeInTheDocument(); + }); + fireEvent.click(screen.getByTestId('statusActive')); + await waitFor(() => { + expect(screen.getByText('Category 1')).toBeInTheDocument(); + expect(screen.queryByText('Category 2')).toBeNull(); + }); + }); + + it('open and closes Create Category modal', async () => { + renderActionItemCategories(link1, 'orgId'); + + const addCategoryBtn = await screen.findByTestId( + 'createActionItemCategoryBtn', + ); + expect(addCategoryBtn).toBeInTheDocument(); + userEvent.click(addCategoryBtn); + + await waitFor(() => expect(screen.getAllByText(t.create)).toHaveLength(2)); + userEvent.click(screen.getByTestId('actionItemCategoryModalCloseBtn')); + await waitFor(() => + expect( + screen.queryByTestId('actionItemCategoryModalCloseBtn'), + ).toBeNull(), + ); + }); + + it('open and closes Edit Category modal', async () => { + renderActionItemCategories(link1, 'orgId'); + + const editCategoryBtn = await screen.findByTestId('editCategoryBtn1'); + await waitFor(() => expect(editCategoryBtn).toBeInTheDocument()); + userEvent.click(editCategoryBtn); + + await waitFor(() => + expect(screen.getByText(t.updateActionItemCategory)).toBeInTheDocument(), + ); + userEvent.click(screen.getByTestId('actionItemCategoryModalCloseBtn')); + await waitFor(() => + expect( + screen.queryByTestId('actionItemCategoryModalCloseBtn'), + ).toBeNull(), + ); + }); + + it('Search categories by name', async () => { + renderActionItemCategories(link1, 'orgId'); + + const searchInput = await screen.findByTestId('searchByName'); + expect(searchInput).toBeInTheDocument(); + + userEvent.type(searchInput, 'Category 1'); + userEvent.click(screen.getByTestId('searchBtn')); + await waitFor(() => { + expect(screen.getByText('Category 1')).toBeInTheDocument(); + expect(screen.queryByText('Category 2')).toBeNull(); + }); + }); + + it('Search categories by name and clear the input by backspace', async () => { + renderActionItemCategories(link1, 'orgId'); + + const searchInput = await screen.findByTestId('searchByName'); + expect(searchInput).toBeInTheDocument(); + + // Clear the search input by backspace + userEvent.type(searchInput, 'A{backspace}'); + await waitFor(() => { + expect(screen.getByText('Category 1')).toBeInTheDocument(); + expect(screen.getByText('Category 2')).toBeInTheDocument(); + }); + }); + + it('Search categories by name on press of ENTER', async () => { + renderActionItemCategories(link1, 'orgId'); + + const searchInput = await screen.findByTestId('searchByName'); + expect(searchInput).toBeInTheDocument(); + + userEvent.type(searchInput, 'Category 1'); + userEvent.type(searchInput, '{enter}'); + await waitFor(() => { + expect(screen.getByText('Category 1')).toBeInTheDocument(); + expect(screen.queryByText('Category 2')).toBeNull(); + }); + }); + + it('should render Empty Action Item Categories Screen', async () => { + renderActionItemCategories(link2, 'orgId'); + await waitFor(() => { + expect(screen.getByTestId('searchByName')).toBeInTheDocument(); + expect(screen.getByText(t.noActionItemCategories)).toBeInTheDocument(); + }); + }); + + it('should render the Action Item Categories Screen with error', async () => { + renderActionItemCategories(link3, 'orgId'); + await waitFor(() => { + expect(screen.getByTestId('errorMsg')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.tsx b/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.tsx new file mode 100644 index 0000000000..49cf47dd49 --- /dev/null +++ b/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.tsx @@ -0,0 +1,418 @@ +import type { FC } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; +import { Button, Dropdown, Form } from 'react-bootstrap'; +import styles from './OrgActionItemCategories.module.css'; +import { useTranslation } from 'react-i18next'; + +import { useQuery } from '@apollo/client'; +import { ACTION_ITEM_CATEGORY_LIST } from 'GraphQl/Queries/Queries'; +import type { InterfaceActionItemCategoryInfo } from 'utils/interfaces'; +import Loader from 'components/Loader/Loader'; +import { + Circle, + Search, + Sort, + WarningAmberRounded, + FilterAltOutlined, +} from '@mui/icons-material'; +import { + DataGrid, + type GridCellParams, + type GridColDef, +} from '@mui/x-data-grid'; +import dayjs from 'dayjs'; +import { Chip, Stack } from '@mui/material'; +import CategoryModal from './CategoryModal'; + +enum ModalState { + SAME = 'same', + DELETE = 'delete', +} + +enum CategoryStatus { + Active = 'active', + Disabled = 'disabled', +} + +interface InterfaceActionItemCategoryProps { + orgId: string; +} + +const dataGridStyle = { + '&.MuiDataGrid-root .MuiDataGrid-cell:focus-within': { + outline: 'none !important', + }, + '&.MuiDataGrid-root .MuiDataGrid-columnHeader:focus-within': { + outline: 'none', + }, + '& .MuiDataGrid-row:hover': { + backgroundColor: 'transparent', + }, + '& .MuiDataGrid-row.Mui-hovered': { + backgroundColor: 'transparent', + }, + '& .MuiDataGrid-root': { + borderRadius: '0.5rem', + }, + '& .MuiDataGrid-main': { + borderRadius: '0.5rem', + }, +}; + +/** + * Represents the component for managing organization action item categories. + * This component allows creating, updating, enabling, and disabling action item categories. + */ +const OrgActionItemCategories: FC = ({ + orgId, +}) => { + const { t } = useTranslation('translation', { + keyPrefix: 'orgActionItemCategories', + }); + const { t: tCommon } = useTranslation('common'); + const { t: tErrors } = useTranslation('errors'); + + const [category, setCategory] = + useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [searchValue, setSearchValue] = useState(''); + const [sortBy, setSortBy] = useState<'createdAt_ASC' | 'createdAt_DESC'>( + 'createdAt_DESC', + ); + const [status, setStatus] = useState(null); + const [categories, setCategories] = useState< + InterfaceActionItemCategoryInfo[] + >([]); + const [modalMode, setModalMode] = useState<'edit' | 'create'>('create'); + const [modalState, setModalState] = useState<{ + [key in ModalState]: boolean; + }>({ + [ModalState.SAME]: false, + [ModalState.DELETE]: false, + }); + + // Query to fetch action item categories + const { + data: catData, + loading: catLoading, + error: catError, + refetch: refetchCategories, + }: { + data?: { + actionItemCategoriesByOrganization: InterfaceActionItemCategoryInfo[]; + }; + loading: boolean; + error?: Error | undefined; + refetch: () => void; + } = useQuery(ACTION_ITEM_CATEGORY_LIST, { + variables: { + organizationId: orgId, + where: { + name_contains: searchTerm, + is_disabled: !status ? undefined : status === CategoryStatus.Disabled, + }, + orderBy: sortBy, + }, + }); + + const openModal = (modal: ModalState): void => + setModalState((prevState) => ({ ...prevState, [modal]: true })); + + const closeModal = (modal: ModalState): void => + setModalState((prevState) => ({ ...prevState, [modal]: false })); + + const handleOpenModal = useCallback( + ( + category: InterfaceActionItemCategoryInfo | null, + mode: 'edit' | 'create', + ): void => { + setCategory(category); + setModalMode(mode); + openModal(ModalState.SAME); + }, + [openModal], + ); + + useEffect(() => { + if (catData && catData.actionItemCategoriesByOrganization) { + setCategories(catData.actionItemCategoriesByOrganization); + } + }, [catData]); + + // Show loader while data is being fetched + if (catLoading) { + return ; + } + + // Show error message if there's an error + if (catError) { + return ( +
+ +
+ {tErrors('errorLoading', { entity: 'Action Item Categories' })} +
+ {`${catError.message}`} +
+
+ ); + } + + const columns: GridColDef[] = [ + { + field: 'id', + headerName: 'Sr. No.', + flex: 1, + minWidth: 100, + align: 'center', + headerAlign: 'center', + headerClassName: `${styles.tableHeader}`, + sortable: false, + renderCell: (params: GridCellParams) => { + return
{params.row.id}
; + }, + }, + { + field: 'categoryName', + headerName: 'Category', + flex: 2, + align: 'center', + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + renderCell: (params: GridCellParams) => { + return ( +
+ {params.row.name} +
+ ); + }, + }, + { + field: 'status', + headerName: 'Status', + flex: 1, + align: 'center', + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + renderCell: (params: GridCellParams) => { + return ( + } + label={params.row.isDisabled ? 'Disabled' : 'Active'} + variant="outlined" + color="primary" + className={`${styles.chip} ${params.row.isDisabled ? styles.pending : styles.active}`} + /> + ); + }, + }, + { + field: 'createdBy', + headerName: 'Created By', + flex: 1, + align: 'center', + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + renderCell: (params: GridCellParams) => { + return params.row.creator.firstName + ' ' + params.row.creator.lastName; + }, + }, + { + field: 'createdOn', + headerName: 'Created On', + align: 'center', + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + flex: 1, + renderCell: (params: GridCellParams) => { + return ( +
+ {dayjs(params.row.createdAt).format('DD/MM/YYYY')} +
+ ); + }, + }, + { + field: 'action', + headerName: 'Action', + flex: 2, + align: 'center', + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + renderCell: (params: GridCellParams) => { + return ( + + ); + }, + }, + ]; + + return ( +
+ {/* Header with search, filter and Create Button */} +
+
+ setSearchValue(e.target.value)} + onKeyUp={(e) => { + if (e.key === 'Enter') { + setSearchTerm(searchValue); + } else if (e.key === 'Backspace' && searchValue === '') { + setSearchTerm(''); + } + }} + data-testid="searchByName" + /> + +
+
+
+ + + + {tCommon('sort')} + + + setSortBy('createdAt_DESC')} + data-testid="createdAt_DESC" + > + {tCommon('createdLatest')} + + setSortBy('createdAt_ASC')} + data-testid="createdAt_ASC" + > + {tCommon('createdEarliest')} + + + + + + + {t('status')} + + + setStatus(null)} + data-testid="statusAll" + > + {tCommon('all')} + + setStatus(CategoryStatus.Active)} + data-testid="statusActive" + > + {tCommon('active')} + + setStatus(CategoryStatus.Disabled)} + data-testid="statusDisabled" + > + {tCommon('disabled')} + + + +
+
+ +
+
+
+ + {/* Table with Action Item Categories */} + row._id} + slots={{ + noRowsOverlay: () => ( + + {t('noActionItemCategories')} + + ), + }} + sx={dataGridStyle} + getRowClassName={() => `${styles.rowBackground}`} + autoHeight + rowHeight={65} + rows={categories.map((category, index) => ({ + id: index + 1, + ...category, + }))} + columns={columns} + isRowSelectable={() => false} + /> + + closeModal(ModalState.SAME)} + refetchCategories={refetchCategories} + category={category} + orgId={orgId} + mode={modalMode} + /> +
+ ); +}; + +export default OrgActionItemCategories; diff --git a/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategoryMocks.ts b/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategoryMocks.ts new file mode 100644 index 0000000000..10310a01e4 --- /dev/null +++ b/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategoryMocks.ts @@ -0,0 +1,288 @@ +import { + CREATE_ACTION_ITEM_CATEGORY_MUTATION, + UPDATE_ACTION_ITEM_CATEGORY_MUTATION, +} from 'GraphQl/Mutations/mutations'; + +import { ACTION_ITEM_CATEGORY_LIST } from 'GraphQl/Queries/Queries'; + +export const MOCKS = [ + { + request: { + query: ACTION_ITEM_CATEGORY_LIST, + variables: { + organizationId: 'orgId', + where: { name_contains: '' }, + orderBy: 'createdAt_DESC', + }, + }, + result: { + data: { + actionItemCategoriesByOrganization: [ + { + _id: 'categoryId1', + name: 'Category 1', + isDisabled: false, + createdAt: '2024-08-26', + creator: { + _id: 'creatorId1', + firstName: 'Wilt', + lastName: 'Shepherd', + }, + }, + { + _id: 'categoryId2', + name: 'Category 2', + isDisabled: true, + createdAt: '2024-08-25', + creator: { + _id: 'creatorId2', + firstName: 'John', + lastName: 'Doe', + }, + }, + ], + }, + }, + }, + { + request: { + query: ACTION_ITEM_CATEGORY_LIST, + variables: { + organizationId: 'orgId', + where: { name_contains: '' }, + orderBy: 'createdAt_ASC', + }, + }, + result: { + data: { + actionItemCategoriesByOrganization: [ + { + _id: 'categoryId2', + name: 'Category 2', + isDisabled: true, + createdAt: '2024-08-25', + creator: { + _id: 'creatorId2', + firstName: 'John', + lastName: 'Doe', + }, + }, + { + _id: 'categoryId1', + name: 'Category 1', + isDisabled: false, + createdAt: '2024-08-26', + creator: { + _id: 'creatorId1', + firstName: 'Wilt', + lastName: 'Shepherd', + }, + }, + ], + }, + }, + }, + { + request: { + query: ACTION_ITEM_CATEGORY_LIST, + variables: { + organizationId: 'orgId', + where: { name_contains: '', is_disabled: false }, + orderBy: 'createdAt_DESC', + }, + }, + result: { + data: { + actionItemCategoriesByOrganization: [ + { + _id: 'categoryId1', + name: 'Category 1', + isDisabled: false, + createdAt: '2024-08-26', + creator: { + _id: 'creatorId1', + firstName: 'Wilt', + lastName: 'Shepherd', + }, + }, + ], + }, + }, + }, + { + request: { + query: ACTION_ITEM_CATEGORY_LIST, + variables: { + organizationId: 'orgId', + where: { name_contains: '', is_disabled: true }, + orderBy: 'createdAt_DESC', + }, + }, + result: { + data: { + actionItemCategoriesByOrganization: [ + { + _id: 'categoryId2', + name: 'Category 2', + isDisabled: true, + createdAt: '2024-08-25', + creator: { + _id: 'creatorId2', + firstName: 'John', + lastName: 'Doe', + }, + }, + ], + }, + }, + }, + { + request: { + query: ACTION_ITEM_CATEGORY_LIST, + variables: { + organizationId: 'orgId', + where: { name_contains: 'Category 1' }, + orderBy: 'createdAt_DESC', + }, + }, + result: { + data: { + actionItemCategoriesByOrganization: [ + { + _id: 'categoryId1', + name: 'Category 1', + isDisabled: false, + createdAt: '2024-08-26', + creator: { + _id: 'creatorId1', + firstName: 'Wilt', + lastName: 'Shepherd', + }, + }, + ], + }, + }, + }, + { + request: { + query: CREATE_ACTION_ITEM_CATEGORY_MUTATION, + variables: { + name: 'Category 2', + isDisabled: true, + organizationId: 'orgId', + }, + }, + result: { + data: { + createActionItemCategory: { + _id: 'categoryId3', + }, + }, + }, + }, + { + request: { + query: UPDATE_ACTION_ITEM_CATEGORY_MUTATION, + variables: { + name: 'Category 2', + isDisabled: true, + actionItemCategoryId: 'categoryId', + }, + }, + result: { + data: { + updateActionItemCategory: { + _id: 'categoryId', + }, + }, + }, + }, + { + request: { + query: UPDATE_ACTION_ITEM_CATEGORY_MUTATION, + variables: { + name: 'Category 2', + isDisabled: false, + actionItemCategoryId: 'categoryId', + }, + }, + result: { + data: { + updateActionItemCategory: { + _id: 'categoryId', + }, + }, + }, + }, + { + request: { + query: UPDATE_ACTION_ITEM_CATEGORY_MUTATION, + variables: { + name: 'Category 1', + isDisabled: true, + actionItemCategoryId: 'categoryId', + }, + }, + result: { + data: { + updateActionItemCategory: { + _id: 'categoryId', + }, + }, + }, + }, +]; + +export const MOCKS_EMPTY = [ + { + request: { + query: ACTION_ITEM_CATEGORY_LIST, + variables: { + organizationId: 'orgId', + where: { name_contains: '' }, + orderBy: 'createdAt_DESC', + }, + }, + result: { + data: { + actionItemCategoriesByOrganization: [], + }, + }, + }, +]; + +export const MOCKS_ERROR = [ + { + request: { + query: ACTION_ITEM_CATEGORY_LIST, + variables: { + organizationId: 'orgId', + where: { name_contains: '' }, + orderBy: 'createdAt_DESC', + }, + }, + error: new Error('Mock Graphql Error'), + }, + { + request: { + query: CREATE_ACTION_ITEM_CATEGORY_MUTATION, + variables: { + name: 'Category 2', + isDisabled: true, + organizationId: 'orgId', + }, + }, + error: new Error('Mock Graphql Error'), + }, + { + request: { + query: UPDATE_ACTION_ITEM_CATEGORY_MUTATION, + variables: { + name: 'Category 2', + isDisabled: true, + actionItemCategoryId: 'categoryId', + }, + }, + error: new Error('Mock Graphql Error'), + }, +]; diff --git a/src/screens/OrganizationAgendaCategory/AgendaCategoryCreateModal.test.tsx b/src/components/OrgSettings/AgendaItemCategories/AgendaCategoryCreateModal.test.tsx similarity index 100% rename from src/screens/OrganizationAgendaCategory/AgendaCategoryCreateModal.test.tsx rename to src/components/OrgSettings/AgendaItemCategories/AgendaCategoryCreateModal.test.tsx diff --git a/src/screens/OrganizationAgendaCategory/AgendaCategoryCreateModal.tsx b/src/components/OrgSettings/AgendaItemCategories/AgendaCategoryCreateModal.tsx similarity index 100% rename from src/screens/OrganizationAgendaCategory/AgendaCategoryCreateModal.tsx rename to src/components/OrgSettings/AgendaItemCategories/AgendaCategoryCreateModal.tsx diff --git a/src/screens/OrganizationAgendaCategory/AgendaCategoryDeleteModal.tsx b/src/components/OrgSettings/AgendaItemCategories/AgendaCategoryDeleteModal.tsx similarity index 100% rename from src/screens/OrganizationAgendaCategory/AgendaCategoryDeleteModal.tsx rename to src/components/OrgSettings/AgendaItemCategories/AgendaCategoryDeleteModal.tsx diff --git a/src/screens/OrganizationAgendaCategory/AgendaCategoryPreviewModal.tsx b/src/components/OrgSettings/AgendaItemCategories/AgendaCategoryPreviewModal.tsx similarity index 100% rename from src/screens/OrganizationAgendaCategory/AgendaCategoryPreviewModal.tsx rename to src/components/OrgSettings/AgendaItemCategories/AgendaCategoryPreviewModal.tsx diff --git a/src/screens/OrganizationAgendaCategory/AgendaCategoryUpdateModal.test.tsx b/src/components/OrgSettings/AgendaItemCategories/AgendaCategoryUpdateModal.test.tsx similarity index 100% rename from src/screens/OrganizationAgendaCategory/AgendaCategoryUpdateModal.test.tsx rename to src/components/OrgSettings/AgendaItemCategories/AgendaCategoryUpdateModal.test.tsx diff --git a/src/screens/OrganizationAgendaCategory/AgendaCategoryUpdateModal.tsx b/src/components/OrgSettings/AgendaItemCategories/AgendaCategoryUpdateModal.tsx similarity index 100% rename from src/screens/OrganizationAgendaCategory/AgendaCategoryUpdateModal.tsx rename to src/components/OrgSettings/AgendaItemCategories/AgendaCategoryUpdateModal.tsx diff --git a/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.module.css b/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.module.css similarity index 100% rename from src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.module.css rename to src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.module.css diff --git a/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.test.tsx b/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.test.tsx similarity index 96% rename from src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.test.tsx rename to src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.test.tsx index 732db13a74..e05edc665d 100644 --- a/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.test.tsx +++ b/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.test.tsx @@ -76,7 +76,7 @@ describe('Testing Agenda Categories Component', () => { - {} + {} @@ -96,7 +96,7 @@ describe('Testing Agenda Categories Component', () => { - {} + {} @@ -119,7 +119,7 @@ describe('Testing Agenda Categories Component', () => { - {} + {} @@ -152,7 +152,7 @@ describe('Testing Agenda Categories Component', () => { - {} + {} diff --git a/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.tsx b/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.tsx similarity index 93% rename from src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.tsx rename to src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.tsx index ed75456b9f..884371d862 100644 --- a/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.tsx +++ b/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.tsx @@ -1,8 +1,7 @@ import React, { useState } from 'react'; -import type { ChangeEvent } from 'react'; +import type { ChangeEvent, FC } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from 'react-bootstrap'; -import { useParams } from 'react-router-dom'; import { WarningAmberRounded } from '@mui/icons-material'; import { toast } from 'react-toastify'; @@ -17,6 +16,10 @@ import AgendaCategoryCreateModal from './AgendaCategoryCreateModal'; import styles from './OrganizationAgendaCategory.module.css'; import Loader from 'components/Loader/Loader'; +interface InterfaceAgendaCategoryProps { + orgId: string; +} + /** * Component for managing and displaying agenda item categories within an organization. * @@ -24,14 +27,14 @@ import Loader from 'components/Loader/Loader'; * * @returns The rendered component. */ -function organizationAgendaCategory(): JSX.Element { + +const organizationAgendaCategory: FC = ({ + orgId, +}) => { const { t } = useTranslation('translation', { keyPrefix: 'organizationAgendaCategory', }); - // Get the organization ID from URL parameters - const { orgId: currentUrl } = useParams(); - // State for managing modal visibility and form data const [agendaCategoryCreateModalIsOpen, setAgendaCategoryCreateModalIsOpen] = useState(false); @@ -56,7 +59,7 @@ function organizationAgendaCategory(): JSX.Element { error?: unknown | undefined; refetch: () => void; } = useQuery(AGENDA_ITEM_CATEGORY_LIST, { - variables: { organizationId: currentUrl }, + variables: { organizationId: orgId }, notifyOnNetworkStatusChange: true, }); @@ -81,7 +84,7 @@ function organizationAgendaCategory(): JSX.Element { await createAgendaCategory({ variables: { input: { - organizationId: currentUrl, + organizationId: orgId, name: formState.name, description: formState.description, }, @@ -132,7 +135,7 @@ function organizationAgendaCategory(): JSX.Element { } return ( -
+
@@ -179,6 +182,6 @@ function organizationAgendaCategory(): JSX.Element { />
); -} +}; export default organizationAgendaCategory; diff --git a/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategoryErrorMocks.ts b/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategoryErrorMocks.ts similarity index 100% rename from src/screens/OrganizationAgendaCategory/OrganizationAgendaCategoryErrorMocks.ts rename to src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategoryErrorMocks.ts diff --git a/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategoryMocks.ts b/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategoryMocks.ts similarity index 100% rename from src/screens/OrganizationAgendaCategory/OrganizationAgendaCategoryMocks.ts rename to src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategoryMocks.ts diff --git a/src/components/DeleteOrg/DeleteOrg.module.css b/src/components/OrgSettings/General/DeleteOrg/DeleteOrg.module.css similarity index 100% rename from src/components/DeleteOrg/DeleteOrg.module.css rename to src/components/OrgSettings/General/DeleteOrg/DeleteOrg.module.css diff --git a/src/components/DeleteOrg/DeleteOrg.test.tsx b/src/components/OrgSettings/General/DeleteOrg/DeleteOrg.test.tsx similarity index 100% rename from src/components/DeleteOrg/DeleteOrg.test.tsx rename to src/components/OrgSettings/General/DeleteOrg/DeleteOrg.test.tsx diff --git a/src/components/DeleteOrg/DeleteOrg.tsx b/src/components/OrgSettings/General/DeleteOrg/DeleteOrg.tsx similarity index 100% rename from src/components/DeleteOrg/DeleteOrg.tsx rename to src/components/OrgSettings/General/DeleteOrg/DeleteOrg.tsx diff --git a/src/components/OrgSettings/General/GeneralSettings.tsx b/src/components/OrgSettings/General/GeneralSettings.tsx new file mode 100644 index 0000000000..4dbca1b6eb --- /dev/null +++ b/src/components/OrgSettings/General/GeneralSettings.tsx @@ -0,0 +1,73 @@ +import React, { type FC } from 'react'; +import { Card, Col, Form, Row } from 'react-bootstrap'; +import styles from 'screens/OrgSettings/OrgSettings.module.css'; +import OrgProfileFieldSettings from './OrgProfileFieldSettings/OrgProfileFieldSettings'; +import ChangeLanguageDropDown from 'components/ChangeLanguageDropdown/ChangeLanguageDropDown'; +import DeleteOrg from './DeleteOrg/DeleteOrg'; +import OrgUpdate from './OrgUpdate/OrgUpdate'; +import { useTranslation } from 'react-i18next'; + +/** + * Props for the `GeneralSettings` component. + */ +interface InterfaceGeneralSettingsProps { + orgId: string; +} + +/** + * A component for displaying general settings for an organization. + * + * @param props - The properties passed to the component. + * @returns The `GeneralSettings` component. + */ +const GeneralSettings: FC = ({ orgId }) => { + const { t } = useTranslation('translation', { + keyPrefix: 'orgSettings', + }); + + return ( + + + +
+
{t('updateOrganization')}
+
+ + {/* Render organization update component */} + + +
+ + + + +
+
{t('otherSettings')}
+
+ +
+ + {t('changeLanguage')} + + {/* Render language change dropdown component */} + +
+
+
+ + + +
+
{t('manageCustomFields')}
+
+ + {/* Render organization profile field settings component */} + + +
+ +
+ ); +}; + +export default GeneralSettings; diff --git a/src/components/OrgProfileFieldSettings/OrgProfileFieldSettings.module.css b/src/components/OrgSettings/General/OrgProfileFieldSettings/OrgProfileFieldSettings.module.css similarity index 100% rename from src/components/OrgProfileFieldSettings/OrgProfileFieldSettings.module.css rename to src/components/OrgSettings/General/OrgProfileFieldSettings/OrgProfileFieldSettings.module.css diff --git a/src/components/OrgProfileFieldSettings/OrgProfileFieldSettings.test.tsx b/src/components/OrgSettings/General/OrgProfileFieldSettings/OrgProfileFieldSettings.test.tsx similarity index 100% rename from src/components/OrgProfileFieldSettings/OrgProfileFieldSettings.test.tsx rename to src/components/OrgSettings/General/OrgProfileFieldSettings/OrgProfileFieldSettings.test.tsx diff --git a/src/components/OrgProfileFieldSettings/OrgProfileFieldSettings.tsx b/src/components/OrgSettings/General/OrgProfileFieldSettings/OrgProfileFieldSettings.tsx similarity index 97% rename from src/components/OrgProfileFieldSettings/OrgProfileFieldSettings.tsx rename to src/components/OrgSettings/General/OrgProfileFieldSettings/OrgProfileFieldSettings.tsx index 8fc9da0290..dcb6992e21 100644 --- a/src/components/OrgProfileFieldSettings/OrgProfileFieldSettings.tsx +++ b/src/components/OrgSettings/General/OrgProfileFieldSettings/OrgProfileFieldSettings.tsx @@ -13,14 +13,7 @@ import { useTranslation } from 'react-i18next'; import { toast } from 'react-toastify'; import EditOrgCustomFieldDropDown from 'components/EditCustomFieldDropDown/EditCustomFieldDropDown'; import { useParams } from 'react-router-dom'; - -/** - * Interface for custom field data - */ -export interface InterfaceCustomFieldData { - type: string; - name: string; -} +import type { InterfaceCustomFieldData } from 'utils/interfaces'; /** * Component for managing organization profile field settings diff --git a/src/components/OrgUpdate/OrgUpdate.module.css b/src/components/OrgSettings/General/OrgUpdate/OrgUpdate.module.css similarity index 100% rename from src/components/OrgUpdate/OrgUpdate.module.css rename to src/components/OrgSettings/General/OrgUpdate/OrgUpdate.module.css diff --git a/src/components/OrgUpdate/OrgUpdate.test.tsx b/src/components/OrgSettings/General/OrgUpdate/OrgUpdate.test.tsx similarity index 100% rename from src/components/OrgUpdate/OrgUpdate.test.tsx rename to src/components/OrgSettings/General/OrgUpdate/OrgUpdate.test.tsx diff --git a/src/components/OrgUpdate/OrgUpdate.tsx b/src/components/OrgSettings/General/OrgUpdate/OrgUpdate.tsx similarity index 100% rename from src/components/OrgUpdate/OrgUpdate.tsx rename to src/components/OrgSettings/General/OrgUpdate/OrgUpdate.tsx diff --git a/src/components/OrgUpdate/OrgUpdateMocks.ts b/src/components/OrgSettings/General/OrgUpdate/OrgUpdateMocks.ts similarity index 100% rename from src/components/OrgUpdate/OrgUpdateMocks.ts rename to src/components/OrgSettings/General/OrgUpdate/OrgUpdateMocks.ts diff --git a/src/screens/EventManagement/EventManagement.module.css b/src/screens/EventManagement/EventManagement.module.css deleted file mode 100644 index f7e911887c..0000000000 --- a/src/screens/EventManagement/EventManagement.module.css +++ /dev/null @@ -1,8 +0,0 @@ -.content { - width: 100%; - height: 100%; - min-height: 80vh; - box-sizing: border-box; - background: #ffffff; - border-radius: 1rem; -} diff --git a/src/screens/EventManagement/EventManagement.tsx b/src/screens/EventManagement/EventManagement.tsx index 74dfc806ce..10207f5c6a 100644 --- a/src/screens/EventManagement/EventManagement.tsx +++ b/src/screens/EventManagement/EventManagement.tsx @@ -1,18 +1,16 @@ import React, { useState } from 'react'; import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; -import styles from './EventManagement.module.css'; import { Navigate, useNavigate, useParams } from 'react-router-dom'; -import { ReactComponent as AngleLeftIcon } from 'assets/svgs/angleLeft.svg'; -import { ReactComponent as EventDashboardIcon } from 'assets/svgs/eventDashboard.svg'; +import { FaChevronLeft, FaTasks } from 'react-icons/fa'; +import { MdOutlineDashboard } from 'react-icons/md'; import { ReactComponent as EventRegistrantsIcon } from 'assets/svgs/people.svg'; -import { ReactComponent as EventActionsIcon } from 'assets/svgs/settings.svg'; +import { IoMdStats } from 'react-icons/io'; import { ReactComponent as EventAgendaItemsIcon } from 'assets/svgs/agenda-items.svg'; -import { ReactComponent as EventStatisticsIcon } from 'assets/svgs/eventStats.svg'; import { useTranslation } from 'react-i18next'; -import { Button } from 'react-bootstrap'; +import { Button, Dropdown } from 'react-bootstrap'; import EventDashboard from 'components/EventManagement/Dashboard/EventDashboard'; -import EventActionItems from 'components/EventManagement/EventActionItems/EventActionItems'; +import OrganizationActionItems from 'screens/OrganizationActionItems/OrganizationActionItems'; import EventAgendaItems from 'components/EventManagement/EventAgendaItems/EventAgendaItems'; import useLocalStorage from 'utils/useLocalstorage'; @@ -27,7 +25,7 @@ const eventDashboardTabs: { }[] = [ { value: 'dashboard', - icon: , + icon: , }, { value: 'registrants', @@ -35,7 +33,7 @@ const eventDashboardTabs: { }, { value: 'eventActions', - icon: , + icon: , }, { value: 'eventAgendas', @@ -43,7 +41,7 @@ const eventDashboardTabs: { }, { value: 'eventStats', - icon: , + icon: , }, ]; @@ -107,15 +105,6 @@ const EventManagement = (): JSX.Element => { // State hook for managing the currently selected tab const [tab, setTab] = useState('dashboard'); - /** - * Handles tab button clicks to update the selected tab. - * - * @param value - The value representing the tab to select - */ - const handleClick = (value: TabOptions): void => { - setTab(value); - }; - /** * Renders a button for each tab with the appropriate icon and label. * @@ -133,14 +122,16 @@ const EventManagement = (): JSX.Element => { const selected = tab === value; const variant = selected ? 'success' : 'light'; const translatedText = t(value); + const className = selected - ? 'px-4' - : 'text-secondary border-secondary-subtle px-4'; + ? 'px-4 d-flex align-items-center shadow' + : 'text-secondary bg-white px-4 d-flex align-items-center rounded shadow'; const props = { variant, className, + style: { height: '2.5rem' }, size: 'sm' as 'sm' | 'lg', - onClick: () => handleClick(value), + onClick: () => setTab(value), 'data-testid': `${value}Btn`, }; @@ -152,66 +143,102 @@ const EventManagement = (): JSX.Element => { ); }; + const handleBack = (): void => { + /*istanbul ignore next*/ + userRole === 'USER' + ? navigate(`/user/events/${orgId}`) + : navigate(`/orgevents/${orgId}`); + }; + return ( -
-
- { - /*istanbul ignore next*/ - userRole === 'USER' - ? navigate(`/user/events/${orgId}`) - : navigate(`/orgevents/${orgId}`); - }} - className="mt-1" - /> -
- {eventDashboardTabs.map(renderButton)} -
-
- - - {/* Render content based on the selected tab */} - {(() => { - switch (tab) { - case 'dashboard': - return ( -
- -
- ); - case 'registrants': - return ( -
-

Event Registrants

-
- ); - case 'eventActions': - return ( -
- -
- ); - case 'eventAgendas': - return ( -
- -
- ); - case 'eventStats': - return ( -
-

Event Statistics

-
- ); - } - })()} +
+ + +
+ + {eventDashboardTabs.map(renderButton)} +
+ + + + {t(tab)} + + + {/* Render dropdown items for each settings category */} + {eventDashboardTabs.map(({ value, icon }, index) => ( + setTab(value) + } + className={`d-flex gap-2 ${tab === value && 'text-secondary'}`} + > + {icon} {t(value)} + + ))} + + + + +
+
+ + {/* Render content based on the selected settings category */} + {(() => { + switch (tab) { + case 'dashboard': + return ( +
+ +
+ ); + case 'registrants': + return ( +
+

Event Registrants

+
+ ); + case 'eventActions': + return ( +
+ +
+ ); + case 'eventAgendas': + return ( +
+ +
+ ); + case 'eventStats': + return ( +
+

Event Statistics

+
+ ); + } + })()}
); }; diff --git a/src/screens/ForgotPassword/ForgotPassword.test.tsx b/src/screens/ForgotPassword/ForgotPassword.test.tsx index b20dfbf767..be1b1706f8 100644 --- a/src/screens/ForgotPassword/ForgotPassword.test.tsx +++ b/src/screens/ForgotPassword/ForgotPassword.test.tsx @@ -1,13 +1,13 @@ -import React, { act } from 'react'; +import React from 'react'; import { MockedProvider } from '@apollo/react-testing'; -import { render, screen } from '@testing-library/react'; +import { act, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import 'jest-localstorage-mock'; import 'jest-location-mock'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; -import { ToastContainer } from 'react-toastify'; +import { toast, ToastContainer } from 'react-toastify'; import { GENERATE_OTP_MUTATION } from 'GraphQl/Mutations/mutations'; import { store } from 'state/store'; @@ -19,6 +19,14 @@ import useLocalStorage from 'utils/useLocalstorage'; const { setItem, removeItem } = useLocalStorage(); +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + }, +})); + const MOCKS = [ { request: { @@ -159,7 +167,9 @@ describe('Testing Forgot Password screen', () => { ); userEvent.click(screen.getByText('Get OTP')); - await wait(); + await waitFor(() => { + expect(toast.success).toHaveBeenCalled(); + }); }); test('Testing forgot password functionality', async () => { @@ -294,7 +304,6 @@ describe('Testing Forgot Password screen', () => { - @@ -310,11 +319,9 @@ describe('Testing Forgot Password screen', () => { ); userEvent.click(screen.getByText('Get OTP')); - await wait(); - - expect( - await screen.findByText(translations.emailNotRegistered), - ).toBeInTheDocument(); + await waitFor(() => { + expect(toast.warn).toHaveBeenCalledWith(translations.emailNotRegistered); + }); }); test('Testing forgot password functionality, when there is an error except unregistered email and api failure', async () => { @@ -323,7 +330,6 @@ describe('Testing Forgot Password screen', () => { - @@ -331,11 +337,9 @@ describe('Testing Forgot Password screen', () => { , ); userEvent.click(screen.getByText('Get OTP')); - await wait(); - - expect( - await screen.findByText(translations.errorSendingMail), - ).toBeInTheDocument(); + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith(translations.errorSendingMail); + }); }); test('Testing forgot password functionality, when talawa api failed', async () => { @@ -347,7 +351,6 @@ describe('Testing Forgot Password screen', () => { - @@ -362,11 +365,11 @@ describe('Testing Forgot Password screen', () => { formData.email, ); userEvent.click(screen.getByText('Get OTP')); - await wait(); - - expect( - await screen.findByText(translations.talawaApiUnavailable), - ).toBeInTheDocument(); + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith( + translations.talawaApiUnavailable, + ); + }); }); test('Testing forgot password functionality, when otp token is not present', async () => { diff --git a/src/screens/ForgotPassword/ForgotPassword.tsx b/src/screens/ForgotPassword/ForgotPassword.tsx index a2e638fd59..ba7b5d21c7 100644 --- a/src/screens/ForgotPassword/ForgotPassword.tsx +++ b/src/screens/ForgotPassword/ForgotPassword.tsx @@ -90,20 +90,16 @@ const ForgotPassword = (): JSX.Element => { }, }); - if (data) { - setItem('otpToken', data.otp.otpToken); - toast.success(t('OTPsent') as string); - setShowEnterEmail(false); - } + setItem('otpToken', data.otp.otpToken); + toast.success(t('OTPsent')); + setShowEnterEmail(false); } catch (error: unknown) { - if (error instanceof Error) { - if (error.message === 'User not found') { - toast.warn(tErrors('emailNotRegistered') as string); - } else if (error.message === 'Failed to fetch') { - toast.error(tErrors('talawaApiUnavailable') as string); - } else { - toast.error(tErrors('errorSendingMail') as string); - } + if ((error as Error).message === 'User not found') { + toast.warn(tErrors('emailNotRegistered')); + } else if ((error as Error).message === 'Failed to fetch') { + toast.error(tErrors('talawaApiUnavailable')); + } else { + toast.error(tErrors('errorSendingMail')); } } }; diff --git a/src/screens/FundCampaignPledge/FundCampaignPledge.test.tsx b/src/screens/FundCampaignPledge/FundCampaignPledge.test.tsx index 25e049e19d..3fb5993775 100644 --- a/src/screens/FundCampaignPledge/FundCampaignPledge.test.tsx +++ b/src/screens/FundCampaignPledge/FundCampaignPledge.test.tsx @@ -85,17 +85,6 @@ describe('Testing Campaign Pledge Screen', () => { jest.clearAllMocks(); }); - afterEach(() => { - cleanup(); - }); - - it('should render the Campaign Pledge screen', async () => { - renderFundCampaignPledge(link1); - await waitFor(() => { - expect(screen.getByTestId('searchPledger')).toBeInTheDocument(); - }); - }); - it('should redirect to fallback URL if URL params are undefined', async () => { render( @@ -122,6 +111,13 @@ describe('Testing Campaign Pledge Screen', () => { }); }); + it('should render the Campaign Pledge screen', async () => { + renderFundCampaignPledge(link1); + await waitFor(() => { + expect(screen.getByTestId('searchPledger')).toBeInTheDocument(); + }); + }); + it('open and closes Create Pledge modal', async () => { renderFundCampaignPledge(link1); @@ -213,6 +209,19 @@ describe('Testing Campaign Pledge Screen', () => { await waitFor(() => { expect(screen.getByTestId('searchPledger')).toBeInTheDocument(); }); + const searchPledger = await screen.findByTestId('searchPledger'); + expect(searchPledger).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('filter')); + await waitFor(() => { + expect(screen.getByTestId('amount_DESC')).toBeInTheDocument(); + }); + fireEvent.click(screen.getByTestId('amount_DESC')); + + await waitFor(() => { + expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.queryByText('Jane Doe')).toBeInTheDocument(); + }); expect(screen.getByText('John Doe')).toBeInTheDocument(); expect(screen.getByText('John Doe2')).toBeInTheDocument(); diff --git a/src/screens/FundCampaignPledge/PledgesMocks.ts b/src/screens/FundCampaignPledge/PledgesMocks.ts index 0dc9c10e56..82cce2a2cf 100644 --- a/src/screens/FundCampaignPledge/PledgesMocks.ts +++ b/src/screens/FundCampaignPledge/PledgesMocks.ts @@ -6,7 +6,48 @@ import { import { MEMBERS_LIST } from 'GraphQl/Queries/Queries'; import { FUND_CAMPAIGN_PLEDGE } from 'GraphQl/Queries/fundQueries'; +const memberList = { + request: { + query: MEMBERS_LIST, + variables: { + id: 'orgId', + }, + }, + result: { + data: { + organizations: [ + { + _id: 'orgId', + members: [ + { + createdAt: '2023-04-13T04:53:17.742Z', + email: 'testuser4@example.com', + firstName: 'John', + image: 'img-url', + lastName: 'Doe', + organizationsBlockedBy: [], + __typename: 'User', + _id: '1', + }, + { + createdAt: '2024-04-13T04:53:17.742Z', + email: 'testuser2@example.com', + firstName: 'Anna', + image: null, + lastName: 'Bradley', + organizationsBlockedBy: [], + __typename: 'User', + _id: '2', + }, + ], + }, + ], + }, + }, +}; + export const MOCKS = [ + memberList, { request: { query: FUND_CAMPAIGN_PLEDGE, @@ -40,60 +81,6 @@ export const MOCKS = [ lastName: 'Doe', image: 'img-url', }, - { - _id: '2', - firstName: 'John', - lastName: 'Doe2', - image: 'img-url2', - }, - { - _id: '3', - firstName: 'John', - lastName: 'Doe3', - image: 'img-url3', - }, - { - _id: '4', - firstName: 'John', - lastName: 'Doe4', - image: 'img-url4', - }, - { - _id: '5', - firstName: 'John', - lastName: 'Doe5', - image: 'img-url5', - }, - { - _id: '6', - firstName: 'John', - lastName: 'Doe6', - image: 'img-url6', - }, - { - _id: '7', - firstName: 'John', - lastName: 'Doe7', - image: 'img-url7', - }, - { - _id: '8', - firstName: 'John', - lastName: 'Doe8', - image: 'img-url8', - }, - { - _id: '9', - firstName: 'John', - lastName: 'Doe9', - image: 'img-url9', - }, - { - _id: '10', - firstName: 'John', - lastName: 'Doe10', - image: null, - }, ], }, { @@ -206,6 +193,60 @@ export const MOCKS = [ lastName: 'Doe', image: null, }, + { + _id: '2', + firstName: 'John', + lastName: 'Doe2', + image: 'img-url2', + }, + { + _id: '3', + firstName: 'John', + lastName: 'Doe3', + image: 'img-url3', + }, + { + _id: '4', + firstName: 'John', + lastName: 'Doe4', + image: 'img-url4', + }, + { + _id: '5', + firstName: 'John', + lastName: 'Doe5', + image: 'img-url5', + }, + { + _id: '6', + firstName: 'John', + lastName: 'Doe6', + image: 'img-url6', + }, + { + _id: '7', + firstName: 'John', + lastName: 'Doe7', + image: 'img-url7', + }, + { + _id: '8', + firstName: 'John', + lastName: 'Doe8', + image: 'img-url8', + }, + { + _id: '9', + firstName: 'John', + lastName: 'Doe9', + image: 'img-url9', + }, + { + _id: '10', + firstName: 'John', + lastName: 'Doe10', + image: null, + }, ], }, { @@ -303,6 +344,7 @@ export const MOCKS = [ ]; export const MOCKS_FUND_CAMPAIGN_PLEDGE_ERROR = [ + memberList, { request: { query: FUND_CAMPAIGN_PLEDGE, @@ -318,6 +360,7 @@ export const MOCKS_FUND_CAMPAIGN_PLEDGE_ERROR = [ ]; export const MOCKS_DELETE_PLEDGE_ERROR = [ + memberList, { request: { query: DELETE_PLEDGE, @@ -330,6 +373,7 @@ export const MOCKS_DELETE_PLEDGE_ERROR = [ ]; export const EMPTY_MOCKS = [ + memberList, { request: { query: FUND_CAMPAIGN_PLEDGE, @@ -358,45 +402,7 @@ export const EMPTY_MOCKS = [ ]; export const PLEDGE_MODAL_MOCKS = [ - { - request: { - query: MEMBERS_LIST, - variables: { - id: 'orgId', - }, - }, - result: { - data: { - organizations: [ - { - _id: 'orgId', - members: [ - { - createdAt: '2023-04-13T04:53:17.742Z', - email: 'testuser4@example.com', - firstName: 'John', - image: 'img-url', - lastName: 'Doe', - organizationsBlockedBy: [], - __typename: 'User', - _id: '1', - }, - { - createdAt: '2024-04-13T04:53:17.742Z', - email: 'testuser2@example.com', - firstName: 'Anna', - image: null, - lastName: 'Bradley', - organizationsBlockedBy: [], - __typename: 'User', - _id: '2', - }, - ], - }, - ], - }, - }, - }, + memberList, { request: { query: UPDATE_PLEDGE, diff --git a/src/screens/OrgSettings/OrgSettings.mocks.ts b/src/screens/OrgSettings/OrgSettings.mocks.ts new file mode 100644 index 0000000000..02748dbf70 --- /dev/null +++ b/src/screens/OrgSettings/OrgSettings.mocks.ts @@ -0,0 +1,143 @@ +import { + ACTION_ITEM_CATEGORY_LIST, + AGENDA_ITEM_CATEGORY_LIST, + IS_SAMPLE_ORGANIZATION_QUERY, + ORGANIZATION_CUSTOM_FIELDS, + ORGANIZATIONS_LIST, +} from 'GraphQl/Queries/Queries'; + +export const MOCKS = [ + { + request: { + query: ORGANIZATIONS_LIST, + variables: { + id: 'orgId', + }, + }, + result: { + data: { + organizations: [ + { + _id: 'orgId', + image: null, + creator: { + firstName: 'Wilt', + lastName: 'Shepherd', + email: 'testsuperadmin@example.com', + __typename: 'User', + }, + name: 'Unity Foundation', + description: + 'A foundation aimed at uniting the world and making it a better place for all.', + address: { + city: 'Bronx', + countryCode: 'US', + dependentLocality: 'Some Dependent Locality', + line1: '123 Random Street', + line2: 'Apartment 456', + postalCode: '10451', + sortingCode: 'ABC-123', + state: 'NYC', + __typename: 'Address', + }, + userRegistrationRequired: false, + visibleInSearch: true, + members: [ + { + _id: '64378abd85008f171cf2990d', + firstName: 'Wilt', + lastName: 'Shepherd', + email: 'testsuperadmin@example.com', + __typename: 'User', + }, + ], + admins: [ + { + _id: '64378abd85008f171cf2990d', + firstName: 'Wilt', + lastName: 'Shepherd', + email: 'testsuperadmin@example.com', + createdAt: '2023-04-13T04:53:17.742Z', + __typename: 'User', + }, + ], + membershipRequests: [], + blockedUsers: [], + __typename: 'Organization', + }, + ], + }, + }, + }, + { + request: { + query: ORGANIZATION_CUSTOM_FIELDS, + variables: { customFieldsByOrganizationId: 'orgId' }, + }, + result: { + data: { + customFieldsByOrganization: [ + { + _id: 'adsdasdsa334343yiu423434', + type: 'fieldType', + name: 'fieldName', + }, + ], + }, + }, + }, + { + request: { + query: IS_SAMPLE_ORGANIZATION_QUERY, + variables: { isSampleOrganizationId: 'orgId' }, + }, + result: { + data: { + isSampleOrganization: false, + }, + }, + }, + + { + request: { + query: AGENDA_ITEM_CATEGORY_LIST, + variables: { organizationId: 'orgId' }, + }, + result: { + data: { + agendaItemCategoriesByOrganization: [], + }, + }, + }, + { + request: { + query: ACTION_ITEM_CATEGORY_LIST, + variables: { + organizationId: 'orgId', + where: { + name_contains: '', + }, + orderBy: 'createdAt_DESC', + }, + }, + result: { + data: { + actionItemCategoriesByOrganization: [ + { + _id: 'actionItemCategoryId1', + name: 'Test 3', + isDisabled: false, + createdAt: '2024-08-25', + creator: { + _id: '64378abd85008f171cf2990d', + firstName: 'Wilt', + lastName: 'Shepherd', + __typename: 'User', + }, + __typename: 'ActionItemCategory', + }, + ], + }, + }, + }, +]; diff --git a/src/screens/OrgSettings/OrgSettings.module.css b/src/screens/OrgSettings/OrgSettings.module.css index 6910ff49ad..9952a9a459 100644 --- a/src/screens/OrgSettings/OrgSettings.module.css +++ b/src/screens/OrgSettings/OrgSettings.module.css @@ -1,3 +1,6 @@ +.headerBtn { + box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 2px; +} .settingsContainer { min-height: 100vh; } diff --git a/src/screens/OrgSettings/OrgSettings.test.tsx b/src/screens/OrgSettings/OrgSettings.test.tsx index efe764da2f..a9aec5f33d 100644 --- a/src/screens/OrgSettings/OrgSettings.test.tsx +++ b/src/screens/OrgSettings/OrgSettings.test.tsx @@ -1,170 +1,107 @@ -import React, { act } from 'react'; +import React from 'react'; import { MockedProvider } from '@apollo/react-testing'; +import type { RenderResult } from '@testing-library/react'; import { render, screen, waitFor } 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 { MemoryRouter, Route, Routes } from 'react-router-dom'; -import { DELETE_ORGANIZATION_MUTATION } from 'GraphQl/Mutations/mutations'; import { store } from 'state/store'; import { StaticMockLink } from 'utils/StaticMockLink'; import i18nForTest from 'utils/i18nForTest'; import OrgSettings from './OrgSettings'; -import { ORGANIZATIONS_LIST } from 'GraphQl/Queries/Queries'; import userEvent from '@testing-library/user-event'; -import useLocalStorage from 'utils/useLocalstorage'; - -const { setItem } = useLocalStorage(); - -const MOCKS = [ - { - request: { - query: ORGANIZATIONS_LIST, - }, - 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: DELETE_ORGANIZATION_MUTATION, - }, - result: { - data: { - removeOrganization: [ - { - _id: 123, - }, - ], - }, - }, - }, -]; - -const link = new StaticMockLink(MOCKS, true); +import type { ApolloLink } from '@apollo/client'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { MOCKS } from './OrgSettings.mocks'; + +const link1 = new StaticMockLink(MOCKS); + +const renderOrganisationSettings = (link: ApolloLink): RenderResult => { + return render( + + + + + + + } /> +
} + /> + + + + + + , + ); +}; -async function wait(ms = 100): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); +describe('Organisation Settings Page', () => { + beforeAll(() => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ orgId: 'orgId' }), + })); }); -} -const translations = JSON.parse( - JSON.stringify(i18nForTest.getDataByLanguage('en')?.translation.orgSettings), -); - -afterEach(() => { - localStorage.clear(); -}); - -describe('Organisation Settings Page', () => { - test('correct mock data should be queried', async () => { - const dataQuery1 = MOCKS[1]?.result?.data?.removeOrganization; - expect(dataQuery1).toEqual([ - { - _id: 123, - }, - ]); + afterAll(() => { + jest.clearAllMocks(); }); - test('should render props and text elements test for the screen', async () => { - window.location.assign('/orgsetting/123'); - setItem('SuperAdmin', true); + it('should redirect to fallback URL if URL params are undefined', async () => { render( - - + + - + + } /> +
} + /> + - + , ); - - await wait(); - - expect(screen.getAllByText(/Delete Organization/i)).toHaveLength(3); - expect( - screen.getByText( - /By clicking on Delete Organization button the organization will be permanently deleted along with its events, tags and all related data/i, - ), - ).toBeInTheDocument(); - expect(screen.getByText(/Other Settings/i)).toBeInTheDocument(); - expect(screen.getByText(/Change Language/i)).toBeInTheDocument(); - expect(window.location).toBeAt('/orgsetting/123'); + await waitFor(() => { + expect(screen.getByTestId('paramsError')).toBeInTheDocument(); + }); }); - test('should render appropriate settings based on the orgSetting state', async () => { - window.location.assign('/orgsetting/123'); - setItem('SuperAdmin', true); + test('should render the organisation settings page', async () => { + renderOrganisationSettings(link1); - const { getAllByText, queryByText } = render( - - - - - - - - - , - ); + await waitFor(() => { + expect(screen.getByTestId('generalSettings')).toBeInTheDocument(); + expect( + screen.getByTestId('actionItemCategoriesSettings'), + ).toBeInTheDocument(); + expect( + screen.getByTestId('agendaItemCategoriesSettings'), + ).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('generalSettings')); - await wait(); + await waitFor(() => { + expect(screen.getByTestId('generalTab')).toBeInTheDocument(); + }); + userEvent.click(screen.getByTestId('actionItemCategoriesSettings')); await waitFor(() => { - userEvent.click(screen.getByTestId('actionItemCategoriesSettings')); - const elements = getAllByText(translations.actionItemCategories); - expect(elements[2]).toBeInTheDocument(); + expect(screen.getByTestId('actionItemCategoriesTab')).toBeInTheDocument(); }); + userEvent.click(screen.getByTestId('agendaItemCategoriesSettings')); await waitFor(() => { - userEvent.click(screen.getByTestId('generalSettings')); - expect(queryByText(translations.updateOrganization)).toBeInTheDocument(); + expect(screen.getByTestId('agendaItemCategoriesTab')).toBeInTheDocument(); }); }); }); diff --git a/src/screens/OrgSettings/OrgSettings.tsx b/src/screens/OrgSettings/OrgSettings.tsx index 336c4eb427..e4ae5424a6 100644 --- a/src/screens/OrgSettings/OrgSettings.tsx +++ b/src/screens/OrgSettings/OrgSettings.tsx @@ -1,18 +1,21 @@ import React, { useState } from 'react'; -import ChangeLanguageDropDown from 'components/ChangeLanguageDropdown/ChangeLanguageDropDown'; -import DeleteOrg from 'components/DeleteOrg/DeleteOrg'; -import OrgUpdate from 'components/OrgUpdate/OrgUpdate'; -import { Button, Card, Dropdown, Form } from 'react-bootstrap'; -import Col from 'react-bootstrap/Col'; -import Row from 'react-bootstrap/Row'; +import { Button, Dropdown, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import styles from './OrgSettings.module.css'; -import OrgProfileFieldSettings from 'components/OrgProfileFieldSettings/OrgProfileFieldSettings'; -import OrgActionItemCategories from 'components/OrgActionItemCategories/OrgActionItemCategories'; -import { useParams } from 'react-router-dom'; +import OrgActionItemCategories from 'components/OrgSettings/ActionItemCategories/OrgActionItemCategories'; +import OrganizationAgendaCategory from 'components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory'; +import { Navigate, useParams } from 'react-router-dom'; +import GeneralSettings from 'components/OrgSettings/General/GeneralSettings'; // Type representing the different settings categories available -type SettingType = 'general' | 'actionItemCategories'; +type SettingType = 'general' | 'actionItemCategories' | 'agendaItemCategories'; + +// List of available settings categories +const settingtabs: SettingType[] = [ + 'general', + 'actionItemCategories', + 'agendaItemCategories', +]; /** * The `orgSettings` component provides a user interface for managing various settings related to an organization. @@ -29,11 +32,7 @@ function orgSettings(): JSX.Element { keyPrefix: 'orgSettings', }); - // List of available settings categories - const orgSettings: SettingType[] = ['general', 'actionItemCategories']; - - // State to manage the currently selected settings category - const [orgSetting, setOrgSetting] = useState('general'); + const [tab, setTab] = useState('general'); // Set the document title using the translated title for this page document.title = t('title'); @@ -41,130 +40,89 @@ function orgSettings(): JSX.Element { // Get the organization ID from the URL parameters const { orgId } = useParams(); + if (!orgId) { + return ; + } + return ( - <> -
- - -
- {/* Render buttons for each settings category */} - {orgSettings.map((setting, index) => ( - + ))} +
+ + {/* Dropdown menu for selecting settings category */} + + + {t(tab)} + + + {/* Render dropdown items for each settings category */} + {settingtabs.map((setting, index) => ( + setOrgSetting(setting)} - data-testid={`${setting}Settings`} + onClick={ + /* istanbul ignore next */ + () => setTab(setting) + } + className={tab === setting ? 'text-secondary' : ''} > {t(setting)} - + ))} -
+ + + - {/* Dropdown menu for selecting settings category */} - - - {t(orgSetting)} - - - {/* Render dropdown items for each settings category */} - {orgSettings.map((setting, index) => ( - setOrgSetting(setting) - } - className={orgSetting === setting ? 'text-secondary' : ''} - > - {t(setting)} - - ))} - - - - - -
-
+ +
+ - {/* Render content based on the selected settings category */} - {orgSetting === 'general' && ( - - - -
-
- {t('updateOrganization')} -
-
- - {/* Render organization update component if orgId is available */} - {orgId && } - -
- - - - -
-
{t('otherSettings')}
-
- -
- - {t('changeLanguage')} - - {/* Render language change dropdown component */} - -
-
-
- - - -
-
- {t('manageCustomFields')} -
-
- - {/* Render organization profile field settings component if orgId is available */} - {orgId && } - -
- -
- )} - - {orgSetting === 'actionItemCategories' && ( - -
-
- {t('actionItemCategories')} + {/* Render content based on the selected settings category */} + {(() => { + switch (tab) { + case 'general': + return ( +
+ +
+ ); + case 'actionItemCategories': + return ( +
+ +
+ ); + case 'agendaItemCategories': + return ( +
+
-
-
- {/* Render action item categories component if orgId is available */} - {orgId && } -
- - )} -
- + ); + } + })()} +
); } diff --git a/src/screens/OrganizationActionItems/ActionItemCreateModal.tsx b/src/screens/OrganizationActionItems/ActionItemCreateModal.tsx deleted file mode 100644 index 00284468f1..0000000000 --- a/src/screens/OrganizationActionItems/ActionItemCreateModal.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React from 'react'; -import { Modal, Form, Button } from 'react-bootstrap'; -import type { ChangeEvent } from 'react'; -import styles from './OrganizationActionItems.module.css'; -import { DatePicker } from '@mui/x-date-pickers'; -import dayjs from 'dayjs'; -import type { Dayjs } from 'dayjs'; - -import type { - InterfaceActionItemCategoryInfo, - InterfaceMemberInfo, -} from 'utils/interfaces'; - -/** - * Interface for the form state used in the `ActionItemCreateModal` component. - */ -interface InterfaceFormStateType { - actionItemCategoryId: string; - assigneeId: string; - eventId?: string; - preCompletionNotes: string; -} - -/** - * Props for the `ActionItemCreateModal` component. - */ -interface InterfaceActionItemCreateModalProps { - actionItemCreateModalIsOpen: boolean; - hideCreateModal: () => void; - formState: InterfaceFormStateType; - setFormState: (state: React.SetStateAction) => void; - createActionItemHandler: (e: ChangeEvent) => Promise; - t: (key: string) => string; - actionItemCategories: InterfaceActionItemCategoryInfo[] | undefined; - membersData: InterfaceMemberInfo[] | undefined; - dueDate: Date | null; - setDueDate: (state: React.SetStateAction) => void; -} - -/** - * A modal component for creating action items. - * - * @param props - The properties passed to the component. - * @returns The `ActionItemCreateModal` component. - */ -const ActionItemCreateModal: React.FC = ({ - actionItemCreateModalIsOpen, - hideCreateModal, - formState, - setFormState, - createActionItemHandler, - t, - actionItemCategories, - membersData, - dueDate, - setDueDate, -}) => { - return ( - <> - - -

{t('actionItemDetails')}

- -
- -
- - {t('actionItemCategory')} - - setFormState({ - ...formState, - actionItemCategoryId: e.target.value, - }) - } - > - - {actionItemCategories?.map((category, index) => ( - - ))} - - - - - {t('assignee')} - - setFormState({ ...formState, assigneeId: e.target.value }) - } - > - - {membersData?.map((member, index) => ( - - ))} - - - - - { - setFormState({ - ...formState, - preCompletionNotes: e.target.value, - }); - }} - /> - -
- { - if (date) { - setDueDate(date?.toDate()); - } - }} - /> -
- - - -
-
- - ); -}; - -export default ActionItemCreateModal; diff --git a/src/screens/OrganizationActionItems/ActionItemDeleteModal.tsx b/src/screens/OrganizationActionItems/ActionItemDeleteModal.tsx deleted file mode 100644 index 22725b5f56..0000000000 --- a/src/screens/OrganizationActionItems/ActionItemDeleteModal.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { Modal, Button } from 'react-bootstrap'; -import styles from './OrganizationActionItems.module.css'; - -/** - * Props for the `ActionItemPreviewModal` component. - */ -interface InterfaceActionItemCreateModalProps { - actionItemDeleteModalIsOpen: boolean; - deleteActionItemHandler: () => Promise; - toggleDeleteModal: () => void; - t: (key: string) => string; - tCommon: (key: string) => string; -} - -/** - * A modal component for confirming the deletion of an action item. - * - * @param props - The properties passed to the component. - * @returns The `ActionItemPreviewModal` component. - */ -const ActionItemPreviewModal: React.FC = ({ - actionItemDeleteModalIsOpen, - deleteActionItemHandler, - toggleDeleteModal, - t, - tCommon, -}) => { - return ( - <> - - - - {t('deleteActionItem')} - - - {t('deleteActionItemMsg')} - - - - - - - ); -}; - -export default ActionItemPreviewModal; diff --git a/src/screens/OrganizationActionItems/ActionItemPreviewModal.tsx b/src/screens/OrganizationActionItems/ActionItemPreviewModal.tsx deleted file mode 100644 index ae2043bf88..0000000000 --- a/src/screens/OrganizationActionItems/ActionItemPreviewModal.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import React from 'react'; -import { Modal, Form, Button } from 'react-bootstrap'; - -import styles from './OrganizationActionItems.module.css'; -import dayjs from 'dayjs'; -/** - * State object for form details related to an action item. - */ -interface InterfaceFormStateType { - assigneeId: string; - assignee: string; - assigner: string; - isCompleted: boolean; - preCompletionNotes: string; - postCompletionNotes: string; -} -/** - * Props for the `ActionItemPreviewModal` component. - */ -interface InterfaceActionItemCreateModalProps { - actionItemPreviewModalIsOpen: boolean; - hidePreviewModal: () => void; - showUpdateModal: () => void; - toggleDeleteModal: () => void; - formState: InterfaceFormStateType; - t: (key: string) => string; - dueDate: Date | null; - completionDate: Date | null; - assignmentDate: Date | null; -} - -/** - * A modal component for previewing the details of an action item. - * - * @param props - The properties passed to the component. - * @returns The `ActionItemPreviewModal` component. - */ -const ActionItemPreviewModal: React.FC = ({ - actionItemPreviewModalIsOpen, - hidePreviewModal, - showUpdateModal, - toggleDeleteModal, - formState, - t, - dueDate, - completionDate, - assignmentDate, -}) => { - return ( - <> - - -

{t('actionItemDetails')}

- -
- -
-
-

- {t('assignee')}:{' '} - {formState.assignee} -

-

- {t('assigner')}:{' '} - {formState.assigner} -

-

- {t('preCompletionNotes')}: - - {formState.preCompletionNotes} - -

-

- {t('postCompletionNotes')}: - - {formState.postCompletionNotes} - -

-

- {t('assignmentDate')}:{' '} - - {dayjs(assignmentDate).format('YYYY-MM-DD')} - -

-

- {t('dueDate')}:{' '} - - {dayjs(dueDate).format('YYYY-MM-DD')} - -

-

- {t('completionDate')}:{' '} - - {dayjs(completionDate).format('YYYY-MM-DD')} - -

-

- {t('status')}:{' '} - - {formState.isCompleted ? 'Completed' : 'Active'} - -

-
-
- - -
-
-
-
- - ); -}; - -export default ActionItemPreviewModal; diff --git a/src/screens/OrganizationActionItems/ActionItemUpdateModal.test.tsx b/src/screens/OrganizationActionItems/ActionItemUpdateModal.test.tsx deleted file mode 100644 index f52da23530..0000000000 --- a/src/screens/OrganizationActionItems/ActionItemUpdateModal.test.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import ActionItemUpdateModal from './ActionItemUpdateModal'; -import { MockedProvider } from '@apollo/react-testing'; -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 { LocalizationProvider } from '@mui/x-date-pickers'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import type { InterfaceMemberInfo } from 'utils/interfaces'; -import { t } from 'i18next'; - -const mockMembersData: InterfaceMemberInfo[] = [ - { - _id: '1', - firstName: 'John', - lastName: 'Doe', - email: 'john.doe@example.com', - image: 'https://example.com/john-doe.jpg', - createdAt: '2022-01-01T00:00:00.000Z', - organizationsBlockedBy: [], - }, - { - _id: '2', - firstName: 'Jane', - lastName: 'Smith', - email: 'jane.smith@example.com', - image: 'https://example.com/jane-smith.jpg', - createdAt: '2022-02-01T00:00:00.000Z', - organizationsBlockedBy: [], - }, -]; - -const mockFormState = { - assigneeId: '1', - assignee: 'John Doe', - assigner: 'Jane Smith', - isCompleted: false, - preCompletionNotes: 'Test pre-completion notes', - postCompletionNotes: '', -}; - -const mockDueDate = new Date('2023-05-01'); -const mockCompletionDate = new Date('2023-05-15'); - -const mockHideUpdateModal = jest.fn(); -const mockSetFormState = jest.fn(); -const mockUpdateActionItemHandler = jest.fn(); -const mockSetDueDate = jest.fn(); -const mockSetCompletionDate = jest.fn(); -const mockT = (key: string): string => key; - -describe('ActionItemUpdateModal', () => { - test('renders modal correctly', () => { - render( - - - - - - - - - - - , - ); - - expect(screen.getByText('actionItemDetails')).toBeInTheDocument(); - expect( - screen.getByTestId('updateActionItemModalCloseBtn'), - ).toBeInTheDocument(); - expect(screen.getByTestId('formUpdateAssignee')).toBeInTheDocument(); - expect(screen.getByLabelText('preCompletionNotes')).toBeInTheDocument(); - expect(screen.getByLabelText('dueDate')).toBeInTheDocument(); - expect(screen.getByLabelText('completionDate')).toBeInTheDocument(); - expect(screen.getByTestId('editActionItemBtn')).toBeInTheDocument(); - }); - - test('closes modal when close button is clicked', () => { - render( - - - - - - - - - - - , - ); - - fireEvent.click(screen.getByTestId('updateActionItemModalCloseBtn')); - expect(mockHideUpdateModal).toHaveBeenCalled(); - }); - - test('updates form state when assignee is changed', () => { - render( - - - - - - - - - - - , - ); - - const assigneeSelect = screen.getByTestId('formUpdateAssignee'); - userEvent.selectOptions(assigneeSelect, '2'); - expect(mockSetFormState).toHaveBeenCalledWith({ - ...mockFormState, - assigneeId: '2', - }); - }); - - test('tests the condition for formState.preCompletionNotes', () => { - const mockFormState = { - assigneeId: '1', - assignee: 'John Doe', - assigner: 'Jane Smith', - isCompleted: false, - preCompletionNotes: '', - postCompletionNotes: '', - }; - render( - - - - - - - - - - - , - ); - const preCompletionNotesInput = screen.getByLabelText('preCompletionNotes'); - fireEvent.change(preCompletionNotesInput, { - target: { value: 'New pre-completion notes' }, - }); - expect(mockSetFormState).toHaveBeenCalledWith({ - ...mockFormState, - preCompletionNotes: 'New pre-completion notes', - }); - }); - - test('calls updateActionItemHandler when form is submitted', () => { - render( - - - - - - - - - - - , - ); - - fireEvent.submit(screen.getByTestId('editActionItemBtn')); - expect(mockUpdateActionItemHandler).toHaveBeenCalled(); - }); -}); diff --git a/src/screens/OrganizationActionItems/ActionItemUpdateModal.tsx b/src/screens/OrganizationActionItems/ActionItemUpdateModal.tsx deleted file mode 100644 index 89a262fe0b..0000000000 --- a/src/screens/OrganizationActionItems/ActionItemUpdateModal.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import React from 'react'; -import { Modal, Form, Button } from 'react-bootstrap'; -import type { ChangeEvent } from 'react'; -import { DatePicker } from '@mui/x-date-pickers'; -import dayjs from 'dayjs'; -import type { Dayjs } from 'dayjs'; - -import styles from './OrganizationActionItems.module.css'; -import type { InterfaceMemberInfo } from 'utils/interfaces'; - -/** - * InterfaceFormStateType is an object containing the form state - */ -interface InterfaceFormStateType { - assigneeId: string; - assignee: string; - assigner: string; - isCompleted: boolean; - preCompletionNotes: string; - postCompletionNotes: string; -} - -/** - * ActionItemUpdateModal component is used to update the action item details like assignee, preCompletionNotes, dueDate, completionDate - * @param actionItemUpdateModalIsOpen - boolean value to check if the modal is open or not - * @param hideUpdateModal - function to hide the modal - * @param formState - object containing the form state - * @param setFormState - function to set the form state - * @param updateActionItemHandler - function to update the action item - * @param t - i18n function to translate the text - * @param membersData - array of members data - * @param dueDate - due date of the action item - * @param setDueDate - function to set the due date - * @param completionDate - completion date of the action item - * @param setCompletionDate - function to set the completion date - * @returns returns the ActionItemUpdateModal component - */ -interface InterfaceActionItemCreateModalProps { - actionItemUpdateModalIsOpen: boolean; - hideUpdateModal: () => void; - formState: InterfaceFormStateType; - setFormState: (state: React.SetStateAction) => void; - updateActionItemHandler: (e: ChangeEvent) => Promise; - t: (key: string) => string; - membersData: InterfaceMemberInfo[] | undefined; - dueDate: Date | null; - setDueDate: (state: React.SetStateAction) => void; - completionDate: Date | null; - setCompletionDate: (state: React.SetStateAction) => void; -} - -const ActionItemUpdateModal: React.FC = ({ - actionItemUpdateModalIsOpen, - hideUpdateModal, - formState, - setFormState, - updateActionItemHandler, - t, - membersData, - dueDate, - setDueDate, - completionDate, - setCompletionDate, -}) => { - return ( - <> - - -

{t('actionItemDetails')}

- -
- -
- - {t('assignee')} - - setFormState({ ...formState, assigneeId: e.target.value }) - } - > - - {membersData?.map((member: InterfaceMemberInfo) => { - const currMemberName = `${member.firstName} ${member.lastName}`; - if (currMemberName !== formState.assignee) { - return ( - - ); - } - })} - - - - - { - setFormState({ - ...formState, - preCompletionNotes: e.target.value, - }); - }} - className="mb-2" - /> - -
- { - /* istanbul ignore next */ - if (date) { - setDueDate(date?.toDate()); - } - } - } - /> -   - { - /* istanbul ignore next */ - if (date) { - setCompletionDate(date?.toDate()); - } - } - } - /> -
- - - -
-
- - ); -}; - -export default ActionItemUpdateModal; diff --git a/src/screens/OrganizationActionItems/ItemDeleteModal.test.tsx b/src/screens/OrganizationActionItems/ItemDeleteModal.test.tsx new file mode 100644 index 0000000000..3d45e12a25 --- /dev/null +++ b/src/screens/OrganizationActionItems/ItemDeleteModal.test.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import type { ApolloLink } from '@apollo/client'; +import { MockedProvider } from '@apollo/react-testing'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import type { RenderResult } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } 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 { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import i18nForTest from '../../utils/i18nForTest'; +import { MOCKS, MOCKS_ERROR } from './OrganizationActionItem.mocks'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import { toast } from 'react-toastify'; +import ItemDeleteModal, { + type InterfaceItemDeleteModalProps, +} from './ItemDeleteModal'; + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +const link1 = new StaticMockLink(MOCKS); +const link2 = new StaticMockLink(MOCKS_ERROR); +const t = JSON.parse( + JSON.stringify( + i18nForTest.getDataByLanguage('en')?.translation.organizationActionItems, + ), +); + +const itemProps: InterfaceItemDeleteModalProps = { + isOpen: true, + hide: jest.fn(), + actionItemsRefetch: jest.fn(), + actionItem: { + _id: 'actionItemId1', + assignee: { + _id: 'userId1', + firstName: 'John', + lastName: 'Doe', + image: null, + }, + actionItemCategory: { + _id: 'actionItemCategoryId1', + name: 'Category 1', + }, + preCompletionNotes: 'Notes 1', + postCompletionNotes: 'Cmp Notes 1', + assignmentDate: new Date('2024-08-27'), + dueDate: new Date('2044-08-30'), + completionDate: new Date('2044-09-03'), + isCompleted: true, + event: null, + allotedHours: 24, + assigner: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + image: null, + }, + creator: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + }, + }, +}; + +const renderItemDeleteModal = ( + link: ApolloLink, + props: InterfaceItemDeleteModalProps, +): RenderResult => { + return render( + + + + + + + + + + + , + ); +}; + +describe('Testing ItemDeleteModal', () => { + it('should render ItemDeleteModal', () => { + renderItemDeleteModal(link1, itemProps); + expect(screen.getByText(t.deleteActionItem)).toBeInTheDocument(); + }); + + it('should successfully Delete Action Item', async () => { + renderItemDeleteModal(link1, itemProps); + expect(screen.getByTestId('deleteyesbtn')).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('deleteyesbtn')); + + await waitFor(() => { + expect(itemProps.actionItemsRefetch).toHaveBeenCalled(); + expect(itemProps.hide).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith(t.successfulDeletion); + }); + }); + + it('should fail to Delete Action Item', async () => { + renderItemDeleteModal(link2, itemProps); + expect(screen.getByTestId('deleteyesbtn')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('deleteyesbtn')); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('Mock Graphql Error'); + }); + }); +}); diff --git a/src/screens/OrganizationActionItems/ItemDeleteModal.tsx b/src/screens/OrganizationActionItems/ItemDeleteModal.tsx new file mode 100644 index 0000000000..2526486993 --- /dev/null +++ b/src/screens/OrganizationActionItems/ItemDeleteModal.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { Modal, Button } from 'react-bootstrap'; +import styles from './OrganizationActionItems.module.css'; +import { useMutation } from '@apollo/client'; +import { DELETE_ACTION_ITEM_MUTATION } from 'GraphQl/Mutations/ActionItemMutations'; +import { toast } from 'react-toastify'; +import { useTranslation } from 'react-i18next'; +import type { InterfaceActionItemInfo } from 'utils/interfaces'; + +/** + * Props for the `ItemDeleteModal` component. + */ +export interface InterfaceItemDeleteModalProps { + isOpen: boolean; + hide: () => void; + actionItem: InterfaceActionItemInfo | null; + actionItemsRefetch: () => void; +} + +/** + * A modal component for confirming the deletion of an action item. + * + * @param props - The properties passed to the component. + * @returns The `ItemDeleteModal` component. + */ +const ItemDeleteModal: React.FC = ({ + isOpen, + hide, + actionItem, + actionItemsRefetch, +}) => { + const { t } = useTranslation('translation', { + keyPrefix: 'organizationActionItems', + }); + const { t: tCommon } = useTranslation('common'); + + const [removeActionItem] = useMutation(DELETE_ACTION_ITEM_MUTATION); + + /** + * Handles the action item deletion. + */ + const deleteActionItemHandler = async (): Promise => { + try { + await removeActionItem({ + variables: { + actionItemId: actionItem?._id, + }, + }); + + actionItemsRefetch(); + hide(); + toast.success(t('successfulDeletion')); + } catch (error: unknown) { + toast.error((error as Error).message); + } + }; + return ( + <> + + +

{t('deleteActionItem')}

+ +
+ +

{t('deleteActionItemMsg')}

+
+ + + + +
+ + ); +}; + +export default ItemDeleteModal; diff --git a/src/screens/OrganizationActionItems/ItemModal.test.tsx b/src/screens/OrganizationActionItems/ItemModal.test.tsx new file mode 100644 index 0000000000..6901e16291 --- /dev/null +++ b/src/screens/OrganizationActionItems/ItemModal.test.tsx @@ -0,0 +1,373 @@ +import React from 'react'; +import type { ApolloLink } from '@apollo/client'; +import { MockedProvider } from '@apollo/react-testing'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import type { RenderResult } from '@testing-library/react'; +import { + fireEvent, + render, + screen, + waitFor, + within, +} 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 { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import i18n from '../../utils/i18nForTest'; +import { MOCKS, MOCKS_ERROR } from './OrganizationActionItem.mocks'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import { toast } from 'react-toastify'; +import type { InterfaceItemModalProps } from './ItemModal'; +import ItemModal from './ItemModal'; + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + warning: jest.fn(), + }, +})); + +const link1 = new StaticMockLink(MOCKS); +const link2 = new StaticMockLink(MOCKS_ERROR); +const t = { + ...JSON.parse( + JSON.stringify( + i18n.getDataByLanguage('en')?.translation.organizationActionItems ?? {}, + ), + ), + ...JSON.parse(JSON.stringify(i18n.getDataByLanguage('en')?.common ?? {})), + ...JSON.parse(JSON.stringify(i18n.getDataByLanguage('en')?.errors ?? {})), +}; + +const itemProps: InterfaceItemModalProps[] = [ + { + isOpen: true, + hide: jest.fn(), + orgId: 'orgId', + actionItemsRefetch: jest.fn(), + editMode: false, + actionItem: null, + }, + { + isOpen: true, + hide: jest.fn(), + orgId: 'orgId', + actionItemsRefetch: jest.fn(), + editMode: true, + actionItem: { + _id: 'actionItemId1', + assignee: { + _id: 'userId1', + firstName: 'Harve', + lastName: 'Lance', + image: '', + }, + actionItemCategory: { + _id: 'categoryId1', + name: 'Category 1', + }, + preCompletionNotes: 'Notes 1', + postCompletionNotes: 'Cmp Notes 1', + assignmentDate: new Date('2024-08-27'), + dueDate: new Date('2044-08-30'), + completionDate: new Date('2044-09-03'), + isCompleted: true, + event: null, + allotedHours: 24, + assigner: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + image: null, + }, + creator: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + }, + }, + }, + { + isOpen: true, + hide: jest.fn(), + orgId: 'orgId', + actionItemsRefetch: jest.fn(), + editMode: true, + actionItem: { + _id: 'actionItemId2', + assignee: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + image: '', + }, + actionItemCategory: { + _id: 'categoryId2', + name: 'Category 2', + }, + preCompletionNotes: 'Notes 2', + postCompletionNotes: null, + assignmentDate: new Date('2024-08-27'), + dueDate: new Date('2044-09-30'), + completionDate: new Date('2044-10-03'), + isCompleted: false, + event: null, + allotedHours: null, + assigner: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + image: 'wilt-image', + }, + creator: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + }, + }, + }, +]; + +const renderItemModal = ( + link: ApolloLink, + props: InterfaceItemModalProps, +): RenderResult => { + return render( + + + + + + + + + + + , + ); +}; + +describe('Testing ItemModal', () => { + it('Create Action Item', async () => { + renderItemModal(link1, itemProps[0]); + expect(screen.getAllByText(t.createActionItem)).toHaveLength(2); + + // Select Category 1 + const categorySelect = await screen.findByTestId('categorySelect'); + expect(categorySelect).toBeInTheDocument(); + const inputField = within(categorySelect).getByRole('combobox'); + fireEvent.mouseDown(inputField); + + const categoryOption = await screen.findByText('Category 1'); + expect(categoryOption).toBeInTheDocument(); + fireEvent.click(categoryOption); + + // Select Assignee + const memberSelect = await screen.findByTestId('memberSelect'); + expect(memberSelect).toBeInTheDocument(); + const memberInputField = within(memberSelect).getByRole('combobox'); + fireEvent.mouseDown(memberInputField); + + const memberOption = await screen.findByText('Harve Lance'); + expect(memberOption).toBeInTheDocument(); + fireEvent.click(memberOption); + + // Select Due Date + fireEvent.change(screen.getByLabelText(t.dueDate), { + target: { value: '02/01/2044' }, + }); + + // Select Allotted Hours (try all options) + const allotedHours = screen.getByLabelText(t.allotedHours); + const allotedHoursOptions = ['', '-1', '9']; + + allotedHoursOptions.forEach((option) => { + fireEvent.change(allotedHours, { target: { value: option } }); + expect(allotedHours).toHaveValue(parseInt(option) > 0 ? option : ''); + }); + + // Add Pre Completion Notes + fireEvent.change(screen.getByLabelText(t.preCompletionNotes), { + target: { value: 'Notes' }, + }); + + // Click Submit + const submitButton = screen.getByTestId('submitBtn'); + expect(submitButton).toBeInTheDocument(); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(itemProps[0].actionItemsRefetch).toHaveBeenCalled(); + expect(itemProps[0].hide).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith(t.successfulCreation); + }); + }); + + it('Update Action Item (completed)', async () => { + renderItemModal(link1, itemProps[1]); + expect(screen.getAllByText(t.updateActionItem)).toHaveLength(2); + + // Update Category + const categorySelect = await screen.findByTestId('categorySelect'); + expect(categorySelect).toBeInTheDocument(); + const inputField = within(categorySelect).getByRole('combobox'); + fireEvent.mouseDown(inputField); + + const categoryOption = await screen.findByText('Category 2'); + expect(categoryOption).toBeInTheDocument(); + fireEvent.click(categoryOption); + + // Update Allotted Hours (try all options) + const allotedHours = screen.getByLabelText(t.allotedHours); + const allotedHoursOptions = ['', '-1', '19']; + + allotedHoursOptions.forEach((option) => { + fireEvent.change(allotedHours, { target: { value: option } }); + expect(allotedHours).toHaveValue(parseInt(option) > 0 ? option : ''); + }); + + // Update Post Completion Notes + fireEvent.change(screen.getByLabelText(t.postCompletionNotes), { + target: { value: 'Cmp Notes 2' }, + }); + + // Click Submit + const submitButton = screen.getByTestId('submitBtn'); + expect(submitButton).toBeInTheDocument(); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(itemProps[1].actionItemsRefetch).toHaveBeenCalled(); + expect(itemProps[1].hide).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith(t.successfulUpdation); + }); + }); + + it('Update Action Item (not completed)', async () => { + renderItemModal(link1, itemProps[2]); + expect(screen.getAllByText(t.updateActionItem)).toHaveLength(2); + + // Update Category + const categorySelect = await screen.findByTestId('categorySelect'); + expect(categorySelect).toBeInTheDocument(); + const inputField = within(categorySelect).getByRole('combobox'); + fireEvent.mouseDown(inputField); + + const categoryOption = await screen.findByText('Category 1'); + expect(categoryOption).toBeInTheDocument(); + fireEvent.click(categoryOption); + + // Update Assignee + const memberSelect = await screen.findByTestId('memberSelect'); + expect(memberSelect).toBeInTheDocument(); + const memberInputField = within(memberSelect).getByRole('combobox'); + fireEvent.mouseDown(memberInputField); + + const memberOption = await screen.findByText('Harve Lance'); + expect(memberOption).toBeInTheDocument(); + fireEvent.click(memberOption); + + // Update Allotted Hours (try all options) + const allotedHours = screen.getByLabelText(t.allotedHours); + const allotedHoursOptions = ['', '-1', '19']; + + allotedHoursOptions.forEach((option) => { + fireEvent.change(allotedHours, { target: { value: option } }); + expect(allotedHours).toHaveValue(parseInt(option) > 0 ? option : ''); + }); + + // Update Due Date + fireEvent.change(screen.getByLabelText(t.dueDate), { + target: { value: '02/01/2044' }, + }); + + // Update Pre Completion Notes + fireEvent.change(screen.getByLabelText(t.preCompletionNotes), { + target: { value: 'Notes 3' }, + }); + + // Click Submit + const submitButton = screen.getByTestId('submitBtn'); + expect(submitButton).toBeInTheDocument(); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(itemProps[2].actionItemsRefetch).toHaveBeenCalled(); + expect(itemProps[2].hide).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith(t.successfulUpdation); + }); + }); + + it('Try adding negative Allotted Hours', async () => { + renderItemModal(link1, itemProps[0]); + expect(screen.getAllByText(t.createActionItem)).toHaveLength(2); + const allotedHours = screen.getByLabelText(t.allotedHours); + fireEvent.change(allotedHours, { target: { value: '-1' } }); + + await waitFor(() => { + expect(allotedHours).toHaveValue(''); + }); + + fireEvent.change(allotedHours, { target: { value: '' } }); + + await waitFor(() => { + expect(allotedHours).toHaveValue(''); + }); + + fireEvent.change(allotedHours, { target: { value: '0' } }); + await waitFor(() => { + expect(allotedHours).toHaveValue('0'); + }); + + fireEvent.change(allotedHours, { target: { value: '19' } }); + await waitFor(() => { + expect(allotedHours).toHaveValue('19'); + }); + }); + + it('should fail to Create Action Item', async () => { + renderItemModal(link2, itemProps[0]); + // Click Submit + const submitButton = screen.getByTestId('submitBtn'); + expect(submitButton).toBeInTheDocument(); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('Mock Graphql Error'); + }); + }); + + it('No Fields Updated while Updating', async () => { + renderItemModal(link2, itemProps[1]); + // Click Submit + const submitButton = screen.getByTestId('submitBtn'); + expect(submitButton).toBeInTheDocument(); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(toast.warning).toHaveBeenCalledWith(t.noneUpdated); + }); + }); + + it('should fail to Update Action Item', async () => { + renderItemModal(link2, itemProps[1]); + expect(screen.getAllByText(t.updateActionItem)).toHaveLength(2); + + // Update Post Completion Notes + fireEvent.change(screen.getByLabelText(t.postCompletionNotes), { + target: { value: 'Cmp Notes 2' }, + }); + + // Click Submit + const submitButton = screen.getByTestId('submitBtn'); + expect(submitButton).toBeInTheDocument(); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('Mock Graphql Error'); + }); + }); +}); diff --git a/src/screens/OrganizationActionItems/ItemModal.tsx b/src/screens/OrganizationActionItems/ItemModal.tsx new file mode 100644 index 0000000000..d7a38ee0df --- /dev/null +++ b/src/screens/OrganizationActionItems/ItemModal.tsx @@ -0,0 +1,440 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Modal, Form, Button } from 'react-bootstrap'; +import type { ChangeEvent, FC } from 'react'; +import styles from './OrganizationActionItems.module.css'; +import { DatePicker } from '@mui/x-date-pickers'; +import dayjs from 'dayjs'; +import type { Dayjs } from 'dayjs'; + +import type { + InterfaceActionItemCategoryInfo, + InterfaceActionItemCategoryList, + InterfaceActionItemInfo, + InterfaceMemberInfo, + InterfaceMembersList, +} from 'utils/interfaces'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'react-toastify'; +import { useMutation, useQuery } from '@apollo/client'; +import { + CREATE_ACTION_ITEM_MUTATION, + UPDATE_ACTION_ITEM_MUTATION, +} from 'GraphQl/Mutations/ActionItemMutations'; +import { ACTION_ITEM_CATEGORY_LIST } from 'GraphQl/Queries/ActionItemCategoryQueries'; +import { MEMBERS_LIST } from 'GraphQl/Queries/Queries'; +import { Autocomplete, FormControl, TextField } from '@mui/material'; + +/** + * Interface for the form state used in the `ItemModal` component. + */ +interface InterfaceFormStateType { + dueDate: Date; + actionItemCategoryId: string; + assigneeId: string; + eventId?: string; + preCompletionNotes: string; + postCompletionNotes: string | null; + allotedHours: number | null; + isCompleted: boolean; +} + +/** + * Props for the `ItemModal` component. + */ +export interface InterfaceItemModalProps { + isOpen: boolean; + hide: () => void; + orgId: string; + actionItemsRefetch: () => void; + actionItem: InterfaceActionItemInfo | null; + editMode: boolean; +} + +/** + * Initializes the form state for the `ItemModal` component. + * + * @param actionItem - The action item to be edited. + * @returns + */ + +const initializeFormState = ( + actionItem: InterfaceActionItemInfo | null, +): InterfaceFormStateType => ({ + dueDate: actionItem?.dueDate || new Date(), + actionItemCategoryId: actionItem?.actionItemCategory?._id || '', + assigneeId: actionItem?.assignee._id || '', + preCompletionNotes: actionItem?.preCompletionNotes || '', + postCompletionNotes: actionItem?.postCompletionNotes || null, + allotedHours: actionItem?.allotedHours || null, + isCompleted: actionItem?.isCompleted || false, +}); + +/** + * A modal component for creating action items. + * + * @param props - The properties passed to the component. + * @returns The `ItemModal` component. + */ +const ItemModal: FC = ({ + isOpen, + hide, + orgId, + actionItem, + editMode, + actionItemsRefetch, +}) => { + const { t } = useTranslation('translation', { + keyPrefix: 'organizationActionItems', + }); + + const [actionItemCategory, setActionItemCategory] = + useState(null); + const [assignee, setAssignee] = useState(null); + + const [formState, setFormState] = useState( + initializeFormState(actionItem), + ); + + const { + dueDate, + actionItemCategoryId, + assigneeId, + preCompletionNotes, + postCompletionNotes, + allotedHours, + isCompleted, + } = formState; + + /** + * Query to fetch action item categories for the organization. + */ + const { + data: actionItemCategoriesData, + }: { + data: InterfaceActionItemCategoryList | undefined; + } = useQuery(ACTION_ITEM_CATEGORY_LIST, { + variables: { + organizationId: orgId, + where: { is_disabled: false }, + }, + }); + + /** + * Query to fetch members of the organization. + */ + const { + data: membersData, + }: { + data: InterfaceMembersList | undefined; + } = useQuery(MEMBERS_LIST, { + variables: { id: orgId }, + }); + + const actionItemCategories = useMemo( + () => actionItemCategoriesData?.actionItemCategoriesByOrganization || [], + [actionItemCategoriesData], + ); + + const members = useMemo( + () => membersData?.organizations[0].members || [], + [membersData], + ); + + /** + * Mutation to create & update a new action item. + */ + const [createActionItem] = useMutation(CREATE_ACTION_ITEM_MUTATION); + const [updateActionItem] = useMutation(UPDATE_ACTION_ITEM_MUTATION); + + /** + * Handler function to update the form state. + * + * @param field - The field to be updated. + * @param value - The value to be set. + * @returns void + */ + const handleFormChange = ( + field: keyof InterfaceFormStateType, + value: string | number | boolean | Date | undefined | null, + ): void => { + setFormState((prevState) => ({ ...prevState, [field]: value })); + }; + + /** + * Handler function to create a new action item. + * + * @param e - The form submit event. + * @returns A promise that resolves when the action item is created. + */ + const createActionItemHandler = async ( + e: ChangeEvent, + ): Promise => { + e.preventDefault(); + try { + await createActionItem({ + variables: { + assigneeId: assignee?._id, + actionItemCategoryId: actionItemCategory?._id, + preCompletionNotes: preCompletionNotes, + allotedHours: allotedHours, + dueDate: dayjs(dueDate).format('YYYY-MM-DD'), + }, + }); + + // Reset form and date after successful creation + setFormState(initializeFormState(null)); + setActionItemCategory(null); + setAssignee(null); + + actionItemsRefetch(); + hide(); + toast.success(t('successfulCreation')); + } catch (error: unknown) { + toast.error((error as Error).message); + } + }; + + /** + * Handles the form submission for updating an action item. + * + * @param e - The form submission event. + */ + const updateActionItemHandler = async ( + e: ChangeEvent, + ): Promise => { + e.preventDefault(); + try { + const updatedFields: { + [key: string]: number | string | boolean | Date | undefined | null; + } = {}; + + if (actionItemCategoryId !== actionItem?.actionItemCategory?._id) { + updatedFields.actionItemCategoryId = actionItemCategoryId; + } + if (assigneeId !== actionItem?.assignee._id) { + updatedFields.assigneeId = assigneeId; + } + + if (preCompletionNotes !== actionItem?.preCompletionNotes) { + updatedFields.preCompletionNotes = preCompletionNotes; + } + + if (postCompletionNotes !== actionItem?.postCompletionNotes) { + updatedFields.postCompletionNotes = postCompletionNotes; + } + + if (allotedHours !== actionItem?.allotedHours) { + updatedFields.allotedHours = allotedHours; + } + + if (dueDate !== actionItem?.dueDate) { + updatedFields.dueDate = dayjs(dueDate).format('YYYY-MM-DD'); + } + + if (Object.keys(updatedFields).length === 0) { + toast.warning(t('noneUpdated')); + return; + } + + await updateActionItem({ + variables: { + actionItemId: actionItem?._id, + assigneeId: assigneeId, + ...updatedFields, + }, + }); + + setFormState(initializeFormState(null)); + actionItemsRefetch(); + hide(); + toast.success(t('successfulUpdation')); + } catch (error: unknown) { + toast.error((error as Error).message); + } + }; + + useEffect(() => { + setFormState(initializeFormState(actionItem)); + setActionItemCategory( + actionItemCategories.find( + (category) => category._id === actionItem?.actionItemCategory?._id, + ) || null, + ); + setAssignee( + members.find((member) => member._id === actionItem?.assignee._id) || null, + ); + }, [actionItem, actionItemCategories, members]); + + return ( + + +

+ {editMode ? t('updateActionItem') : t('createActionItem')} +

+ +
+ +
+ + option._id === value._id} + filterSelectedOptions={true} + getOptionLabel={(item: InterfaceActionItemCategoryInfo): string => + item.name + } + onChange={(_, newCategory): void => { + /* istanbul ignore next */ + handleFormChange( + 'actionItemCategoryId', + newCategory?._id ?? '', + ); + setActionItemCategory(newCategory); + }} + renderInput={(params) => ( + + )} + /> + {isCompleted && ( + <> + {/* Input text Component to add alloted Hours for action item */} + + + handleFormChange( + 'allotedHours', + e.target.value === '' || parseInt(e.target.value) < 0 + ? null + : parseInt(e.target.value), + ) + } + /> + + + )} + + {!isCompleted && ( + <> + + + option._id === value._id + } + filterSelectedOptions={true} + getOptionLabel={(member: InterfaceMemberInfo): string => + `${member.firstName} ${member.lastName}` + } + onChange={(_, newAssignee): void => { + /* istanbul ignore next */ + handleFormChange('assigneeId', newAssignee?._id ?? ''); + setAssignee(newAssignee); + }} + renderInput={(params) => ( + + )} + /> + + + + {/* Date Calendar Component to select due date of an action item */} + { + /* istanbul ignore next */ + if (date) handleFormChange('dueDate', date.toDate()); + }} + /> + + {/* Input text Component to add alloted Hours for action item */} + + + handleFormChange( + 'allotedHours', + e.target.value === '' || parseInt(e.target.value) < 0 + ? null + : parseInt(e.target.value), + ) + } + /> + + + + {/* Input text Component to add notes for action item */} + + + handleFormChange('preCompletionNotes', e.target.value) + } + /> + + + )} + + {isCompleted && ( + + + handleFormChange('postCompletionNotes', e.target.value) + } + /> + + )} + + +
+
+
+ ); +}; + +export default ItemModal; diff --git a/src/screens/OrganizationActionItems/ItemUpdateStatusModal.test.tsx b/src/screens/OrganizationActionItems/ItemUpdateStatusModal.test.tsx new file mode 100644 index 0000000000..c1fee119cc --- /dev/null +++ b/src/screens/OrganizationActionItems/ItemUpdateStatusModal.test.tsx @@ -0,0 +1,173 @@ +import React from 'react'; +import type { ApolloLink } from '@apollo/client'; +import { MockedProvider } from '@apollo/react-testing'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import type { RenderResult } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } 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 { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import i18nForTest from '../../utils/i18nForTest'; +import { MOCKS, MOCKS_ERROR } from './OrganizationActionItem.mocks'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import { toast } from 'react-toastify'; +import ItemUpdateStatusModal, { + type InterfaceItemUpdateStatusModalProps, +} from './ItemUpdateStatusModal'; + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +const link1 = new StaticMockLink(MOCKS); +const link2 = new StaticMockLink(MOCKS_ERROR); +const t = JSON.parse( + JSON.stringify( + i18nForTest.getDataByLanguage('en')?.translation.organizationActionItems, + ), +); + +const itemProps: InterfaceItemUpdateStatusModalProps[] = [ + { + isOpen: true, + hide: jest.fn(), + actionItemsRefetch: jest.fn(), + actionItem: { + _id: 'actionItemId1', + assignee: { + _id: 'userId1', + firstName: 'John', + lastName: 'Doe', + image: null, + }, + actionItemCategory: { + _id: 'actionItemCategoryId1', + name: 'Category 1', + }, + preCompletionNotes: 'Notes 1', + postCompletionNotes: 'Cmp Notes 1', + assignmentDate: new Date('2024-08-27'), + dueDate: new Date('2044-08-30'), + completionDate: new Date('2044-09-03'), + isCompleted: true, + event: null, + allotedHours: 24, + assigner: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + image: null, + }, + creator: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + }, + }, + }, + { + isOpen: true, + hide: jest.fn(), + actionItemsRefetch: jest.fn(), + actionItem: { + _id: 'actionItemId1', + assignee: { + _id: 'userId1', + firstName: 'John', + lastName: 'Doe', + image: null, + }, + actionItemCategory: { + _id: 'actionItemCategoryId1', + name: 'Category 1', + }, + preCompletionNotes: 'Notes 1', + postCompletionNotes: null, + assignmentDate: new Date('2024-08-27'), + dueDate: new Date('2044-08-30'), + completionDate: new Date('2044-09-03'), + isCompleted: false, + event: null, + allotedHours: 24, + assigner: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + image: null, + }, + creator: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + }, + }, + }, +]; + +const renderItemUpdateStatusModal = ( + link: ApolloLink, + props: InterfaceItemUpdateStatusModalProps, +): RenderResult => { + return render( + + + + + + + + + + + , + ); +}; + +describe('Testing ItemUpdateStatusModal', () => { + it('Update Status of Completed ActionItem', async () => { + renderItemUpdateStatusModal(link1, itemProps[0]); + expect(screen.getByText(t.actionItemStatus)).toBeInTheDocument(); + const yesBtn = await screen.findByTestId('yesBtn'); + fireEvent.click(yesBtn); + + await waitFor(() => { + expect(itemProps[0].actionItemsRefetch).toHaveBeenCalled(); + expect(itemProps[0].hide).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith(t.successfulUpdation); + }); + }); + + it('Update Status of Pending ActionItem', async () => { + renderItemUpdateStatusModal(link1, itemProps[1]); + expect(screen.getByText(t.actionItemStatus)).toBeInTheDocument(); + + const notes = await screen.findByLabelText(t.postCompletionNotes); + fireEvent.change(notes, { target: { value: 'Cmp Notes 1' } }); + + const createBtn = await screen.findByTestId('createBtn'); + fireEvent.click(createBtn); + + await waitFor(() => { + expect(itemProps[1].actionItemsRefetch).toHaveBeenCalled(); + expect(itemProps[1].hide).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith(t.successfulUpdation); + }); + }); + + it('should fail to Update status of Action Item', async () => { + renderItemUpdateStatusModal(link2, itemProps[0]); + + expect(screen.getByText(t.actionItemStatus)).toBeInTheDocument(); + const yesBtn = await screen.findByTestId('yesBtn'); + fireEvent.click(yesBtn); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('Mock Graphql Error'); + }); + }); +}); diff --git a/src/screens/OrganizationActionItems/ItemUpdateStatusModal.tsx b/src/screens/OrganizationActionItems/ItemUpdateStatusModal.tsx new file mode 100644 index 0000000000..44ac0e63e6 --- /dev/null +++ b/src/screens/OrganizationActionItems/ItemUpdateStatusModal.tsx @@ -0,0 +1,129 @@ +import React, { type FC, type FormEvent, useEffect, useState } from 'react'; +import { Modal, Button, Form } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import { FormControl, TextField } from '@mui/material'; +import styles from './OrganizationActionItems.module.css'; +import { useMutation } from '@apollo/client'; +import { UPDATE_ACTION_ITEM_MUTATION } from 'GraphQl/Mutations/ActionItemMutations'; +import { toast } from 'react-toastify'; +import type { InterfaceActionItemInfo } from 'utils/interfaces'; + +export interface InterfaceItemUpdateStatusModalProps { + isOpen: boolean; + hide: () => void; + actionItemsRefetch: () => void; + actionItem: InterfaceActionItemInfo | null; +} + +const ItemUpdateStatusModal: FC = ({ + hide, + isOpen, + actionItemsRefetch, + actionItem, +}) => { + const { t } = useTranslation('translation', { + keyPrefix: 'organizationActionItems', + }); + const { t: tCommon } = useTranslation('common'); + + const [isCompleted, setIsCompleted] = useState( + actionItem?.isCompleted ?? false, + ); + const [postCompletionNotes, setPostCompletionNotes] = useState( + actionItem?.postCompletionNotes ?? '', + ); + + /** + * Mutation to update an action item. + */ + const [updateActionItem] = useMutation(UPDATE_ACTION_ITEM_MUTATION); + + /** + * Handles the form submission for updating an action item. + * + * @param e - The form submission event. + */ + const updateActionItemHandler = async ( + e: FormEvent, + ): Promise => { + e.preventDefault(); + + try { + await updateActionItem({ + variables: { + actionItemId: actionItem?._id, + assigneeId: actionItem?.assignee?._id, + postCompletionNotes: isCompleted ? '' : postCompletionNotes, + isCompleted: !isCompleted, + }, + }); + + actionItemsRefetch(); + hide(); + toast.success(t('successfulUpdation')); + } catch (error: unknown) { + toast.error((error as Error).message); + } + }; + + useEffect(() => { + if (actionItem) { + setIsCompleted(actionItem?.isCompleted); + setPostCompletionNotes(actionItem?.postCompletionNotes ?? ''); + } + }, [actionItem]); + + return ( + + +

{t('actionItemStatus')}

+ +
+ +
+ {!isCompleted ? ( + + setPostCompletionNotes(e.target.value)} + /> + + ) : ( +

{t('updateStatusMsg')}

+ )} + + {isCompleted ? ( +
+ + +
+ ) : ( + + )} +
+
+
+ ); +}; + +export default ItemUpdateStatusModal; diff --git a/src/screens/OrganizationActionItems/ItemViewModal.test.tsx b/src/screens/OrganizationActionItems/ItemViewModal.test.tsx new file mode 100644 index 0000000000..13c208b854 --- /dev/null +++ b/src/screens/OrganizationActionItems/ItemViewModal.test.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import type { ApolloLink } from '@apollo/client'; +import { MockedProvider } from '@apollo/react-testing'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import type { RenderResult } from '@testing-library/react'; +import { 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 { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import i18nForTest from '../../utils/i18nForTest'; +import { MOCKS } from './OrganizationActionItem.mocks'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import ItemViewModal, { type InterfaceViewModalProps } from './ItemViewModal'; + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +const link1 = new StaticMockLink(MOCKS); +const t = JSON.parse( + JSON.stringify( + i18nForTest.getDataByLanguage('en')?.translation.organizationActionItems, + ), +); + +const itemProps: InterfaceViewModalProps[] = [ + { + isOpen: true, + hide: jest.fn(), + item: { + _id: 'actionItemId1', + assignee: { + _id: 'userId1', + firstName: 'John', + lastName: 'Doe', + image: null, + }, + actionItemCategory: { + _id: 'actionItemCategoryId1', + name: 'Category 1', + }, + preCompletionNotes: 'Notes 1', + postCompletionNotes: 'Cmp Notes 1', + assignmentDate: new Date('2024-08-27'), + dueDate: new Date('2044-08-30'), + completionDate: new Date('2044-09-03'), + isCompleted: true, + event: null, + allotedHours: 24, + assigner: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + image: null, + }, + creator: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + }, + }, + }, + { + isOpen: true, + hide: jest.fn(), + item: { + _id: 'actionItemId2', + assignee: { + _id: 'userId1', + firstName: 'Jane', + lastName: 'Doe', + image: 'image-url', + }, + actionItemCategory: { + _id: 'actionItemCategoryId2', + name: 'Category 2', + }, + preCompletionNotes: 'Notes 2', + postCompletionNotes: null, + assignmentDate: new Date('2024-08-27'), + dueDate: new Date('2044-09-30'), + completionDate: new Date('2044-10-03'), + isCompleted: false, + event: null, + allotedHours: null, + assigner: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + image: 'wilt-image', + }, + creator: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + }, + }, + }, +]; + +const renderItemViewModal = ( + link: ApolloLink, + props: InterfaceViewModalProps, +): RenderResult => { + return render( + + + + + + + + + + + , + ); +}; + +describe('Testing ItemViewModal', () => { + it('should render ItemViewModal with pending item & assignee with null image', () => { + renderItemViewModal(link1, itemProps[0]); + expect(screen.getByText(t.actionItemDetails)).toBeInTheDocument(); + expect(screen.getByTestId('John_avatar')).toBeInTheDocument(); + expect(screen.getByTestId('Wilt_avatar')).toBeInTheDocument(); + expect(screen.getByLabelText(t.postCompletionNotes)).toBeInTheDocument(); + expect(screen.getByLabelText(t.allotedHours)).toBeInTheDocument(); + expect(screen.getByLabelText(t.allotedHours)).toHaveValue('24'); + }); + + it('should render ItemViewModal with completed item & assignee with null image', () => { + renderItemViewModal(link1, itemProps[1]); + expect(screen.getByText(t.actionItemDetails)).toBeInTheDocument(); + expect(screen.getByTestId('Jane_image')).toBeInTheDocument(); + expect(screen.getByTestId('Wilt_image')).toBeInTheDocument(); + expect( + screen.queryByLabelText(t.postCompletionNotes), + ).not.toBeInTheDocument(); + expect(screen.getByLabelText(t.allotedHours)).toBeInTheDocument(); + expect(screen.getByLabelText(t.allotedHours)).toHaveValue('-'); + }); +}); diff --git a/src/screens/OrganizationActionItems/ItemViewModal.tsx b/src/screens/OrganizationActionItems/ItemViewModal.tsx new file mode 100644 index 0000000000..84d9a3e7fe --- /dev/null +++ b/src/screens/OrganizationActionItems/ItemViewModal.tsx @@ -0,0 +1,233 @@ +import { DatePicker } from '@mui/x-date-pickers'; +import React from 'react'; +import dayjs from 'dayjs'; +import type { FC } from 'react'; +import { Button, Form, Modal } from 'react-bootstrap'; +import type { InterfaceActionItemInfo } from 'utils/interfaces'; +import styles from './OrganizationActionItems.module.css'; +import { useTranslation } from 'react-i18next'; +import { FormControl, TextField } from '@mui/material'; +import { TaskAlt, HistoryToggleOff } from '@mui/icons-material'; +import Avatar from 'components/Avatar/Avatar'; + +export interface InterfaceViewModalProps { + isOpen: boolean; + hide: () => void; + item: InterfaceActionItemInfo; +} + +/** + * A modal dialog for viewing action item details. + * + * @param isOpen - Indicates whether the modal is open. + * @param hide - Function to close the modal. + * @param item - The action item object to be displayed. + * + * @returns The rendered modal component. + * + * The `ItemViewModal` component displays all the fields of an action item in a modal dialog. + * It includes fields for assignee, assigner, category, pre and post completion notes, assignment date, due date, completion date, and event. + */ + +const ItemViewModal: FC = ({ isOpen, hide, item }) => { + const { t } = useTranslation('translation', { + keyPrefix: 'organizationActionItems', + }); + const { t: tCommon } = useTranslation('common'); + + const { + actionItemCategory, + assignee, + assigner, + completionDate, + dueDate, + isCompleted, + postCompletionNotes, + preCompletionNotes, + allotedHours, + } = item; + + return ( + + +

{t('actionItemDetails')}

+ +
+ +
+ + + + + + + + + {assignee.image ? ( + Assignee + ) : ( +
+ +
+ )} + + ), + }} + /> +
+ + + {assigner.image ? ( + Assignee + ) : ( +
+ +
+ )} + + ), + }} + /> +
+
+ + {/* Status of Action Item */} + + {isCompleted ? ( + + ) : ( + + )} + + ), + style: { + color: isCompleted ? 'green' : '#ed6c02', + }, + }} + inputProps={{ + style: { + WebkitTextFillColor: isCompleted ? 'green' : '#ed6c02', + }, + }} + disabled + /> + + + + + {/* Date Calendar Component to display due date of Action Item */} + + + {/* Date Calendar Component to display completion Date of Action Item */} + {isCompleted && ( + + )} + + + + + + + {isCompleted && ( + + + + )} +
+
+
+ ); +}; +export default ItemViewModal; diff --git a/src/screens/OrganizationActionItems/OrganizationActionItem.mocks.ts b/src/screens/OrganizationActionItems/OrganizationActionItem.mocks.ts new file mode 100644 index 0000000000..0a324d39ac --- /dev/null +++ b/src/screens/OrganizationActionItems/OrganizationActionItem.mocks.ts @@ -0,0 +1,482 @@ +import dayjs from 'dayjs'; +import { + CREATE_ACTION_ITEM_MUTATION, + DELETE_ACTION_ITEM_MUTATION, + UPDATE_ACTION_ITEM_MUTATION, +} from 'GraphQl/Mutations/ActionItemMutations'; +import { + ACTION_ITEM_CATEGORY_LIST, + ACTION_ITEM_LIST, + MEMBERS_LIST, +} from 'GraphQl/Queries/Queries'; +import { act } from 'react-dom/test-utils'; + +const baseActionItem = { + assigner: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + image: null, + }, + creator: { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + __typename: 'User', + }, +}; + +const actionItem1 = { + _id: 'actionItemId1', + assignee: { + _id: 'userId1', + firstName: 'John', + lastName: 'Doe', + image: null, + }, + actionItemCategory: { + _id: 'actionItemCategoryId1', + name: 'Category 1', + }, + preCompletionNotes: 'Notes 1', + postCompletionNotes: 'Cmp Notes 1', + assignmentDate: '2024-08-27', + dueDate: '2044-08-30', + completionDate: '2044-09-03', + isCompleted: true, + event: null, + allotedHours: 24, + ...baseActionItem, +}; + +const actionItem2 = { + _id: 'actionItemId2', + assignee: { + _id: 'userId1', + firstName: 'Jane', + lastName: 'Doe', + image: 'image-url', + }, + actionItemCategory: { + _id: 'actionItemCategoryId2', + name: 'Category 2', + }, + preCompletionNotes: 'Notes 2', + postCompletionNotes: null, + assignmentDate: '2024-08-27', + dueDate: '2044-09-30', + completionDate: '2044-10-03', + isCompleted: false, + event: null, + allotedHours: null, + ...baseActionItem, +}; + +const memberListQuery = { + request: { + query: MEMBERS_LIST, + variables: { id: 'orgId' }, + }, + result: { + data: { + organizations: [ + { + _id: 'orgId', + members: [ + { + _id: 'userId1', + firstName: 'Harve', + lastName: 'Lance', + email: 'harve@example.com', + image: '', + organizationsBlockedBy: [], + createdAt: '2024-02-14', + }, + { + _id: 'userId2', + firstName: 'Wilt', + lastName: 'Shepherd', + email: 'wilt@example.com', + image: '', + organizationsBlockedBy: [], + createdAt: '2024-02-14', + }, + ], + }, + ], + }, + }, +}; + +const actionItemCategoryListQuery = { + request: { + query: ACTION_ITEM_CATEGORY_LIST, + variables: { + organizationId: 'orgId', + where: { is_disabled: false }, + }, + }, + result: { + data: { + actionItemCategoriesByOrganization: [ + { + _id: 'categoryId1', + name: 'Category 1', + isDisabled: false, + createdAt: '2024-08-26', + creator: { + _id: 'creatorId1', + firstName: 'Wilt', + lastName: 'Shepherd', + }, + }, + { + _id: 'categoryId2', + name: 'Category 2', + isDisabled: true, + createdAt: '2024-08-25', + creator: { + _id: 'creatorId2', + firstName: 'John', + lastName: 'Doe', + }, + }, + ], + }, + }, +}; + +export const MOCKS = [ + { + request: { + query: ACTION_ITEM_LIST, + variables: { + organizationId: 'orgId', + orderBy: null, + where: { + assigneeName: '', + }, + }, + }, + result: { + data: { + actionItemsByOrganization: [actionItem1, actionItem2], + }, + }, + }, + { + request: { + query: ACTION_ITEM_LIST, + variables: { + organizationId: 'orgId', + orderBy: null, + where: { + categoryName: '', + }, + }, + }, + result: { + data: { + actionItemsByOrganization: [actionItem1, actionItem2], + }, + }, + }, + { + request: { + query: ACTION_ITEM_LIST, + variables: { + organizationId: 'orgId', + orderBy: 'dueDate_ASC', + where: { + assigneeName: '', + }, + }, + }, + result: { + data: { + actionItemsByOrganization: [actionItem1, actionItem2], + }, + }, + }, + { + request: { + query: ACTION_ITEM_LIST, + variables: { + organizationId: 'orgId', + orderBy: 'dueDate_DESC', + where: { + assigneeName: '', + }, + }, + }, + result: { + data: { + actionItemsByOrganization: [actionItem2, actionItem1], + }, + }, + }, + { + request: { + query: ACTION_ITEM_LIST, + variables: { + organizationId: 'orgId', + orderBy: null, + where: { + assigneeName: '', + is_completed: true, + }, + }, + }, + result: { + data: { + actionItemsByOrganization: [actionItem1], + }, + }, + }, + { + request: { + query: ACTION_ITEM_LIST, + variables: { + organizationId: 'orgId', + orderBy: null, + where: { + assigneeName: '', + is_completed: false, + }, + }, + }, + result: { + data: { + actionItemsByOrganization: [actionItem2], + }, + }, + }, + { + request: { + query: ACTION_ITEM_LIST, + variables: { + organizationId: 'orgId', + orderBy: null, + where: { + assigneeName: 'John', + }, + }, + }, + result: { + data: { + actionItemsByOrganization: [actionItem1], + }, + }, + }, + { + request: { + query: ACTION_ITEM_LIST, + variables: { + organizationId: 'orgId', + orderBy: null, + where: { + categoryName: 'Category 1', + }, + }, + }, + result: { + data: { + actionItemsByOrganization: [actionItem1], + }, + }, + }, + { + request: { + query: DELETE_ACTION_ITEM_MUTATION, + variables: { + actionItemId: 'actionItemId1', + }, + }, + result: { + data: { + removeActionItem: { + _id: 'actionItemId1', + }, + }, + }, + }, + { + request: { + query: UPDATE_ACTION_ITEM_MUTATION, + variables: { + actionItemId: 'actionItemId1', + assigneeId: 'userId1', + postCompletionNotes: '', + isCompleted: false, + }, + }, + result: { + data: { + updateActionItem: { + _id: 'actionItemId1', + }, + }, + }, + }, + { + request: { + query: UPDATE_ACTION_ITEM_MUTATION, + variables: { + actionItemId: 'actionItemId1', + assigneeId: 'userId1', + actionItemCategoryId: 'categoryId2', + postCompletionNotes: 'Cmp Notes 2', + allotedHours: 19, + }, + }, + result: { + data: { + updateActionItem: { + _id: 'actionItemId1', + }, + }, + }, + }, + { + request: { + query: UPDATE_ACTION_ITEM_MUTATION, + variables: { + actionItemId: 'actionItemId2', + assigneeId: 'userId1', + actionItemCategoryId: 'categoryId1', + preCompletionNotes: 'Notes 3', + allotedHours: 19, + dueDate: '2044-01-02', + }, + }, + result: { + data: { + updateActionItem: { + _id: 'actionItemId1', + }, + }, + }, + }, + { + request: { + query: UPDATE_ACTION_ITEM_MUTATION, + variables: { + actionItemId: 'actionItemId1', + assigneeId: 'userId1', + postCompletionNotes: 'Cmp Notes 1', + isCompleted: true, + }, + }, + result: { + data: { + updateActionItem: { + _id: 'actionItemId1', + }, + }, + }, + }, + { + request: { + query: CREATE_ACTION_ITEM_MUTATION, + variables: { + assigneeId: 'userId1', + actionItemCategoryId: 'categoryId1', + preCompletionNotes: 'Notes', + allotedHours: 9, + dueDate: '2044-01-02', + }, + }, + result: { + data: { + createActionItem: { + _id: 'actionItemId1', + }, + }, + }, + }, + memberListQuery, + actionItemCategoryListQuery, +]; + +export const MOCKS_ERROR = [ + { + request: { + query: ACTION_ITEM_LIST, + variables: { + organizationId: 'orgId', + orderBy: null, + where: { + assigneeName: '', + }, + }, + }, + error: new Error('Mock Graphql Error'), + }, + { + request: { + query: DELETE_ACTION_ITEM_MUTATION, + variables: { + actionItemId: 'actionItemId1', + }, + }, + error: new Error('Mock Graphql Error'), + }, + { + request: { + query: UPDATE_ACTION_ITEM_MUTATION, + variables: { + actionItemId: 'actionItemId1', + assigneeId: 'userId1', + postCompletionNotes: '', + isCompleted: false, + }, + }, + error: new Error('Mock Graphql Error'), + }, + { + request: { + query: CREATE_ACTION_ITEM_MUTATION, + variables: { + preCompletionNotes: '', + allotedHours: null, + dueDate: dayjs().format('YYYY-MM-DD'), + }, + }, + error: new Error('Mock Graphql Error'), + }, + { + request: { + query: UPDATE_ACTION_ITEM_MUTATION, + variables: { + actionItemId: 'actionItemId1', + assigneeId: 'userId1', + postCompletionNotes: 'Cmp Notes 2', + }, + }, + error: new Error('Mock Graphql Error'), + }, + memberListQuery, + actionItemCategoryListQuery, +]; + +export const MOCKS_EMPTY = [ + { + request: { + query: ACTION_ITEM_LIST, + variables: { + organizationId: 'orgId', + orderBy: null, + where: { + assigneeName: '', + }, + }, + }, + result: { + data: { + actionItemsByOrganization: [], + }, + }, + }, + memberListQuery, + actionItemCategoryListQuery, +]; diff --git a/src/screens/OrganizationActionItems/OrganizationActionItemMocks.ts b/src/screens/OrganizationActionItems/OrganizationActionItemMocks.ts deleted file mode 100644 index acbb243e5d..0000000000 --- a/src/screens/OrganizationActionItems/OrganizationActionItemMocks.ts +++ /dev/null @@ -1,417 +0,0 @@ -import { CREATE_ACTION_ITEM_MUTATION } from 'GraphQl/Mutations/mutations'; - -import { - ACTION_ITEM_CATEGORY_LIST, - ACTION_ITEM_LIST, - MEMBERS_LIST, -} from 'GraphQl/Queries/Queries'; - -export const MOCKS = [ - { - request: { - query: ACTION_ITEM_CATEGORY_LIST, - variables: { organizationId: '123' }, - }, - result: { - data: { - actionItemCategoriesByOrganization: [ - { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - isDisabled: false, - }, - ], - }, - }, - }, - { - request: { - query: MEMBERS_LIST, - variables: { id: '123' }, - }, - result: { - data: { - organizations: [ - { - _id: '123', - members: [ - { - _id: 'user1', - firstName: 'Harve', - lastName: 'Lance', - email: 'harve@example.com', - image: '', - organizationsBlockedBy: [], - createdAt: '2024-02-14', - }, - ], - }, - ], - }, - }, - }, - { - request: { - query: ACTION_ITEM_LIST, - variables: { - organizationId: '123', - orderBy: 'createdAt_DESC', - actionItemCategoryId: '', - isActive: false, - isCompleted: false, - }, - }, - result: { - data: { - actionItemsByOrganization: [ - { - _id: 'actionItem1', - assignee: { - _id: 'user1', - firstName: 'Harve', - lastName: 'Lance', - }, - actionItemCategory: { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - }, - preCompletionNotes: 'Pre Completion Notes', - postCompletionNotes: 'Post Completion Notes', - assignmentDate: '2024-02-14', - dueDate: '2024-02-21', - completionDate: '2024-02-21', - isCompleted: false, - assigner: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - _id: 'event1', - title: 'event 1', - }, - creator: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - }, - { - _id: 'actionItem2', - assignee: { - _id: 'user2', - firstName: 'John', - lastName: 'Doe', - }, - actionItemCategory: { - _id: 'actionItemCategory2', - name: 'ActionItemCategory 2', - }, - preCompletionNotes: 'Pre Completion Notes', - postCompletionNotes: 'Post Completion Notes', - assignmentDate: '2024-02-14', - dueDate: '2024-02-21', - completionDate: '2024-02-21', - isCompleted: false, - assigner: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: null, - creator: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - }, - ], - }, - }, - }, - { - request: { - query: ACTION_ITEM_LIST, - variables: { - organizationId: '123', - eventId: 'event1', - orderBy: 'createdAt_DESC', - }, - }, - result: { - data: { - actionItemsByOrganization: [ - { - _id: 'actionItem1', - assignee: { - _id: 'user1', - firstName: 'Harve', - lastName: 'Lance', - }, - actionItemCategory: { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - }, - preCompletionNotes: 'Pre Completion Notes', - postCompletionNotes: 'Post Completion Notes', - assignmentDate: '2024-02-14', - dueDate: '2024-02-21', - completionDate: '2024-02-21', - isCompleted: false, - assigner: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - _id: 'event1', - title: 'event 1', - }, - creator: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - }, - ], - }, - }, - }, - { - request: { - query: ACTION_ITEM_LIST, - variables: { - organizationId: '123', - orderBy: 'createdAt_ASC', - actionItemCategoryId: '', - isActive: false, - isCompleted: false, - }, - }, - result: { - data: { - actionItemsByOrganization: [ - { - _id: 'actionItem3', - assignee: { - _id: 'user1', - firstName: 'Praise', - lastName: 'Norris', - }, - actionItemCategory: { - _id: 'actionItemCategory3', - name: 'ActionItemCategory 3', - }, - preCompletionNotes: 'Pre Completion Notes', - postCompletionNotes: 'Post Completion Notes', - assignmentDate: '2024-02-14', - dueDate: '2024-02-21', - completionDate: '2024-02-21', - isCompleted: false, - assigner: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - _id: 'event1', - title: 'event 1', - }, - creator: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - }, - ], - }, - }, - }, - { - request: { - query: ACTION_ITEM_LIST, - variables: { - organizationId: '123', - orderBy: 'createdAt_ASC', - actionItemCategoryId: '', - isActive: true, - isCompleted: false, - }, - }, - result: { - data: { - actionItemsByOrganization: [ - { - _id: 'actionItem1', - assignee: { - _id: 'user1', - firstName: 'Harve', - lastName: 'Lance', - }, - actionItemCategory: { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - }, - preCompletionNotes: 'Pre Completion Notes', - postCompletionNotes: 'Post Completion Notes', - assignmentDate: '2024-02-14', - dueDate: '2024-02-21', - completionDate: '2024-02-21', - isCompleted: true, - assigner: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - _id: 'event1', - title: 'event 1', - }, - creator: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - }, - ], - }, - }, - }, - { - request: { - query: ACTION_ITEM_LIST, - variables: { - organizationId: '123', - orderBy: 'createdAt_ASC', - actionItemCategoryId: '', - isActive: false, - isCompleted: true, - }, - }, - result: { - data: { - actionItemsByOrganization: [ - { - _id: 'actionItem1', - assignee: { - _id: 'user1', - firstName: 'Harve', - lastName: 'Lance', - }, - actionItemCategory: { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - }, - preCompletionNotes: 'Pre Completion Notes', - postCompletionNotes: 'Post Completion Notes', - assignmentDate: '2024-02-14', - dueDate: '2024-02-21', - completionDate: '2024-02-21', - isCompleted: true, - assigner: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - _id: 'event1', - title: 'event 1', - }, - creator: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - }, - ], - }, - }, - }, - { - request: { - query: ACTION_ITEM_LIST, - variables: { - organizationId: '123', - orderBy: 'createdAt_ASC', - actionItemCategoryId: 'actionItemCategory1', - isActive: false, - isCompleted: true, - }, - }, - result: { - data: { - actionItemsByOrganization: [ - { - _id: 'actionItem1', - assignee: { - _id: 'user1', - firstName: 'Harve', - lastName: 'Lance', - }, - actionItemCategory: { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - }, - preCompletionNotes: 'Pre Completion Notes', - postCompletionNotes: 'Post Completion Notes', - assignmentDate: '2024-02-14', - dueDate: '2024-02-21', - completionDate: '2024-02-21', - isCompleted: true, - assigner: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - _id: 'event1', - title: 'event 1', - }, - creator: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - }, - ], - }, - }, - }, - { - request: { - query: CREATE_ACTION_ITEM_MUTATION, - variables: { - actionItemCategoryId: 'actionItemCategory1', - assigneeId: 'user1', - preCompletionNotes: 'pre completion notes', - dueDate: '2024-02-14', - }, - }, - result: { - data: { - createActionItem: { - _id: 'actionItem2', - }, - }, - }, - }, - { - request: { - query: CREATE_ACTION_ITEM_MUTATION, - variables: { - eventId: 'event1', - actionItemCategoryId: 'actionItemCategory1', - assigneeId: 'user1', - preCompletionNotes: 'pre completion notes', - dueDate: '2024-02-14', - }, - }, - result: { - data: { - createActionItem: { - _id: 'actionItem2', - }, - }, - }, - }, -]; diff --git a/src/screens/OrganizationActionItems/OrganizationActionItems.module.css b/src/screens/OrganizationActionItems/OrganizationActionItems.module.css index 7a67362c8b..48720ac902 100644 --- a/src/screens/OrganizationActionItems/OrganizationActionItems.module.css +++ b/src/screens/OrganizationActionItems/OrganizationActionItems.module.css @@ -8,26 +8,6 @@ margin-left: 13vw; } -.btnsContainer { - display: flex; - gap: 10px; -} - -.btnsContainer .btnsBlock { - display: flex; - gap: 10px; -} - -.btnsContainer button { - display: flex; - justify-content: center; - align-items: center; -} - -.container { - min-height: 100vh; -} - .datediv { display: flex; flex-direction: row; @@ -46,10 +26,6 @@ margin-left: 5px; } -.dropdown { - display: block; -} - .dropdownToggle { margin-bottom: 0; display: flex; @@ -108,10 +84,6 @@ hr { flex-direction: column; } -.organizationActionItemsContainer h2 { - margin: 0.6rem 0; -} - .preview { display: flex; flex-direction: row; @@ -128,16 +100,6 @@ hr { display: inline; } -.titlemodal { - color: var(--bs-gray-600); - font-weight: 600; - font-size: 20px; - margin-bottom: 20px; - padding-bottom: 5px; - border-bottom: 3px solid var(--bs-primary); - width: 65%; -} - .view { margin-left: 2%; font-weight: 600; @@ -145,24 +107,127 @@ hr { color: var(--bs-gray-600); } -@media (max-width: 767px) { - .btnsContainer { - margin-bottom: 0; - display: flex; - flex-direction: column; - } +/* header (search, filter, dropdown) */ +.btnsContainer { + display: flex; + margin: 0.5rem 0 1.5rem 0; +} + +.btnsContainer .input { + flex: 1; + min-width: 18rem; + position: relative; +} + +.btnsContainer input { + outline: 1px solid var(--bs-gray-400); +} + +.btnsContainer .input button { + width: 52px; +} + +.noOutline input { + outline: none; +} + +.noOutline input:disabled { + -webkit-text-fill-color: black !important; +} + +.noOutline textarea:disabled { + -webkit-text-fill-color: black !important; +} + +.inputField { + margin-top: 10px; + margin-bottom: 10px; + background-color: white; + box-shadow: 0 1px 1px #31bb6b; +} + +.inputField > button { + padding-top: 10px; + padding-bottom: 10px; +} - .btnsContainer .btnsBlock .dropdownToggle { - flex-grow: 1; - } +.dropdown { + background-color: white; + border: 1px solid #31bb6b; + position: relative; + display: inline-block; + color: #31bb6b; +} - .btnsContainer button { - width: 100%; - } +/* Action Items Data Grid */ +.rowBackground { + background-color: var(--bs-white); + max-height: 120px; +} + +.tableHeader { + background-color: var(--bs-primary); + color: var(--bs-white); + font-size: 1rem; +} + +.chipIcon { + height: 0.9rem !important; +} + +.chip { + height: 1.5rem !important; +} + +.active { + background-color: #31bb6a50 !important; +} + +.pending { + background-color: #ffd76950 !important; + color: #bb952bd0 !important; + border-color: #bb952bd0 !important; +} + +/* Modals */ +.itemModal { + max-width: 80vw; + margin-top: 2vh; + margin-left: 13vw; +} + +.titlemodal { + color: #707070; + font-weight: 600; + font-size: 32px; + width: 65%; + margin-bottom: 0px; +} + +.modalCloseBtn { + width: 40px; + height: 40px; + padding: 1rem; + display: flex; + justify-content: center; + align-items: center; +} + +.imageContainer { + display: flex; + align-items: center; + justify-content: center; + margin-right: 0.5rem; +} + +.TableImage { + object-fit: cover; + width: 25px !important; + height: 25px !important; + border-radius: 100% !important; +} - .createActionItemButton { - position: absolute; - top: 1rem; - right: 2rem; - } +.avatarContainer { + width: 28px; + height: 26px; } diff --git a/src/screens/OrganizationActionItems/OrganizationActionItems.test.tsx b/src/screens/OrganizationActionItems/OrganizationActionItems.test.tsx index 7a9060a892..c163ff9546 100644 --- a/src/screens/OrganizationActionItems/OrganizationActionItems.test.tsx +++ b/src/screens/OrganizationActionItems/OrganizationActionItems.test.tsx @@ -1,35 +1,23 @@ import React from 'react'; -import { - render, - screen, - fireEvent, - waitFor, - act, - waitForElementToBeRemoved, -} from '@testing-library/react'; +import { MockedProvider } from '@apollo/react-testing'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import type { RenderResult } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import 'jest-localstorage-mock'; -import { MockedProvider } from '@apollo/client/testing'; -import 'jest-location-mock'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; -import i18n from 'utils/i18nForTest'; -import { toast } from 'react-toastify'; -import { LocalizationProvider } from '@mui/x-date-pickers'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; - +import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { store } from 'state/store'; import { StaticMockLink } from 'utils/StaticMockLink'; - -import OrganizationActionItems from './OrganizationActionItems'; +import i18n from 'utils/i18nForTest'; +import OrganizationActionItems from 'screens/OrganizationActionItems/OrganizationActionItems'; +import type { ApolloLink } from '@apollo/client'; import { - MOCKS_ERROR_ACTION_ITEM_CATEGORY_LIST_QUERY, - MOCKS_ERROR_ACTION_ITEM_LIST_QUERY, - MOCKS_ERROR_MEMBERS_LIST_QUERY, - MOCKS_ERROR_MUTATIONS, -} from './OrganizationActionItemsErrorMocks'; -import { MOCKS } from './OrganizationActionItemMocks'; + MOCKS, + MOCKS_EMPTY, + MOCKS_ERROR, +} from './OrganizationActionItem.mocks'; jest.mock('react-toastify', () => ({ toast: { @@ -46,29 +34,10 @@ jest.mock('@mui/x-date-pickers/DateTimePicker', () => { }; }); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: '123' }), -})); - -async function wait(ms = 100): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); -} - -const link = new StaticMockLink(MOCKS, true); -const link2 = new StaticMockLink( - MOCKS_ERROR_ACTION_ITEM_CATEGORY_LIST_QUERY, - true, -); -const link3 = new StaticMockLink(MOCKS_ERROR_MEMBERS_LIST_QUERY, true); -const link4 = new StaticMockLink(MOCKS_ERROR_ACTION_ITEM_LIST_QUERY, true); -const link5 = new StaticMockLink(MOCKS_ERROR_MUTATIONS, true); - -const translations = { +const link1 = new StaticMockLink(MOCKS); +const link2 = new StaticMockLink(MOCKS_ERROR); +const link3 = new StaticMockLink(MOCKS_EMPTY); +const t = { ...JSON.parse( JSON.stringify( i18n.getDataByLanguage('en')?.translation.organizationActionItems ?? {}, @@ -78,542 +47,331 @@ const translations = { ...JSON.parse(JSON.stringify(i18n.getDataByLanguage('en')?.errors ?? {})), }; -describe('Testing Action Item Categories Component', () => { - const formData = { - actionItemCategory: 'ActionItemCategory 1', - assignee: 'Harve Lance', - preCompletionNotes: 'pre completion notes', - dueDate: '02/14/2024', - }; - - test('Component loads correctly', async () => { - const { getByText } = render( - +const renderOrganizationActionItems = (link: ApolloLink): RenderResult => { + return render( + + - + - {} + + } + /> +
} + /> + - + - , - ); - - await wait(); + + , + ); +}; - await waitFor(() => { - expect(getByText(translations.create)).toBeInTheDocument(); - }); +describe('Testing Organization Action Items Screen', () => { + beforeAll(() => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ orgId: 'orgId', eventId: 'eventId' }), + })); }); - test('render error component on unsuccessful action item category list query', async () => { - const { queryByText } = render( - - - - - {} - - - - , - ); - - await wait(); - - await waitFor(() => { - expect(queryByText(translations.create)).not.toBeInTheDocument(); - }); + afterAll(() => { + jest.clearAllMocks(); }); - test('render error component on unsuccessful members list query', async () => { - const { queryByText } = render( - - - + it('should redirect to fallback URL if URL params are undefined', async () => { + render( + + + - {} + + } + /> +
} + /> + - - + + , ); - - await wait(); - await waitFor(() => { - expect(queryByText(translations.create)).not.toBeInTheDocument(); + expect(screen.getByTestId('paramsError')).toBeInTheDocument(); }); }); - test('render error component on unsuccessful action item list query', async () => { - const { queryByText } = render( - - - - - {} - - - - , - ); - - await wait(); - + it('should render Organization Action Items screen', async () => { + renderOrganizationActionItems(link1); await waitFor(() => { - expect(queryByText(translations.create)).not.toBeInTheDocument(); + expect(screen.getByTestId('searchBy')).toBeInTheDocument(); + expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.getByText('Jane Doe')).toBeInTheDocument(); }); }); - test('sorts action items in earliest or latest first order based on orderBy', async () => { - render( - - - - - {} - - - - , - ); + it('Sort Action Items descending by dueDate', async () => { + renderOrganizationActionItems(link1); - await wait(); + const sortBtn = await screen.findByTestId('sort'); + expect(sortBtn).toBeInTheDocument(); + // Sort by dueDate_DESC + fireEvent.click(sortBtn); await waitFor(() => { - expect(screen.getByTestId('sortActionItems')).toBeInTheDocument(); + expect(screen.getByTestId('dueDate_DESC')).toBeInTheDocument(); }); - userEvent.click(screen.getByTestId('sortActionItems')); - + fireEvent.click(screen.getByTestId('dueDate_DESC')); await waitFor(() => { - expect(screen.getByTestId('earliest')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('earliest')); - - await waitFor(() => { - expect(screen.getByTestId('sortActionItems')).toHaveTextContent( - translations.earliest, - ); - }); - - await waitFor(() => { - expect(screen.getByTestId('sortActionItems')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('sortActionItems')); - - await waitFor(() => { - expect(screen.getByTestId('latest')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('latest')); - - await waitFor(() => { - expect(screen.getByTestId('sortActionItems')).toHaveTextContent( - translations.latest, + expect(screen.getAllByTestId('categoryName')[0]).toHaveTextContent( + 'Category 2', ); }); }); - test('applies and then clears filters one by one', async () => { - render( - - - - - {} - - - - , - ); - - await wait(); + it('Sort Action Items ascending by dueDate', async () => { + renderOrganizationActionItems(link1); - await waitFor(() => { - expect(screen.getByTestId('sortActionItems')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('sortActionItems')); + const sortBtn = await screen.findByTestId('sort'); + expect(sortBtn).toBeInTheDocument(); + // Sort by dueDate_ASC + fireEvent.click(sortBtn); await waitFor(() => { - expect(screen.getByTestId('earliest')).toBeInTheDocument(); + expect(screen.getByTestId('dueDate_ASC')).toBeInTheDocument(); }); - userEvent.click(screen.getByTestId('earliest')); - - // all the action items ordered by earliest first + fireEvent.click(screen.getByTestId('dueDate_ASC')); await waitFor(() => { - expect(screen.getByTestId('sortActionItems')).toHaveTextContent( - translations.earliest, + expect(screen.getAllByTestId('categoryName')[0]).toHaveTextContent( + 'Category 1', ); }); + }); - await waitFor(() => { - expect(screen.getByTestId('selectActionItemStatus')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('selectActionItemStatus')); - - await waitFor(() => { - expect(screen.getByTestId('activeActionItems')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('activeActionItems')); + it('Filter Action Items by status (All/Pending)', async () => { + renderOrganizationActionItems(link1); - // all the action items that are active - await waitFor(() => { - expect(screen.getByTestId('selectActionItemStatus')).toHaveTextContent( - translations.active, - ); - }); + const filterBtn = await screen.findByTestId('filter'); + expect(filterBtn).toBeInTheDocument(); + // Filter by All + fireEvent.click(filterBtn); await waitFor(() => { - expect(screen.getByTestId('selectActionItemStatus')).toBeInTheDocument(); + expect(screen.getByTestId('statusAll')).toBeInTheDocument(); }); - userEvent.click(screen.getByTestId('selectActionItemStatus')); + fireEvent.click(screen.getByTestId('statusAll')); await waitFor(() => { - expect(screen.getByTestId('completedActionItems')).toBeInTheDocument(); + expect(screen.getByText('Category 1')).toBeInTheDocument(); + expect(screen.getByText('Category 2')).toBeInTheDocument(); }); - userEvent.click(screen.getByTestId('completedActionItems')); - // all the action items that are completed + // Filter by Pending + fireEvent.click(filterBtn); await waitFor(() => { - expect(screen.getByTestId('selectActionItemStatus')).toHaveTextContent( - translations.completed, - ); + expect(screen.getByTestId('statusPending')).toBeInTheDocument(); }); - + fireEvent.click(screen.getByTestId('statusPending')); await waitFor(() => { - expect( - screen.getByTestId('selectActionItemCategory'), - ).toBeInTheDocument(); + expect(screen.queryByText('Category 1')).toBeNull(); + expect(screen.getByText('Category 2')).toBeInTheDocument(); }); - userEvent.click(screen.getByTestId('selectActionItemCategory')); + }); - await waitFor(() => { - expect( - screen.getAllByTestId('actionItemCategory')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('actionItemCategory')[0]); + it('Filter Action Items by status (Completed)', async () => { + renderOrganizationActionItems(link1); - // action items belonging to this action item category - await waitFor(() => { - expect(screen.getByTestId('selectActionItemCategory')).toHaveTextContent( - 'ActionItemCategory 1', - ); - }); + const filterBtn = await screen.findByTestId('filter'); + expect(filterBtn).toBeInTheDocument(); + fireEvent.click(filterBtn); await waitFor(() => { - expect( - screen.getByTestId('clearActionItemCategoryFilter'), - ).toBeInTheDocument(); + expect(screen.getByTestId('statusCompleted')).toBeInTheDocument(); }); - // remove the action item category filter - userEvent.click(screen.getByTestId('clearActionItemCategoryFilter')); - + fireEvent.click(screen.getByTestId('statusCompleted')); await waitFor(() => { - expect( - screen.queryByTestId('clearActionItemCategoryFilter'), - ).not.toBeInTheDocument(); + expect(screen.getByText('Category 1')).toBeInTheDocument(); + expect(screen.queryByText('Category 2')).toBeNull(); }); + }); - await waitFor(() => { - expect( - screen.getByTestId('clearActionItemStatusFilter'), - ).toBeInTheDocument(); - }); - // remove the action item status filter - userEvent.click(screen.getByTestId('clearActionItemStatusFilter')); + it('open and close Item modal (create)', async () => { + renderOrganizationActionItems(link1); - await waitFor(() => { - expect(screen.getByTestId('selectActionItemStatus')).toHaveTextContent( - translations.status, - ); - expect(screen.getByTestId('selectActionItemCategory')).toHaveTextContent( - translations.actionItemCategory, - ); - }); - }); + const addItemBtn = await screen.findByTestId('createActionItemBtn'); + expect(addItemBtn).toBeInTheDocument(); + userEvent.click(addItemBtn); - test('applies and then clears all the filters', async () => { - render( - - - - - {} - - - - , + await waitFor(() => + expect(screen.getAllByText(t.createActionItem)).toHaveLength(2), + ); + userEvent.click(screen.getByTestId('modalCloseBtn')); + await waitFor(() => + expect(screen.queryByTestId('modalCloseBtn')).toBeNull(), ); + }); - await wait(); + it('open and close Item modal (view)', async () => { + renderOrganizationActionItems(link1); - await waitFor(() => { - expect(screen.getByTestId('sortActionItems')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('sortActionItems')); + const viewItemBtn = await screen.findByTestId('viewItemBtn1'); + expect(viewItemBtn).toBeInTheDocument(); + userEvent.click(viewItemBtn); - await waitFor(() => { - expect(screen.getByTestId('earliest')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('earliest')); + await waitFor(() => + expect(screen.getByText(t.actionItemDetails)).toBeInTheDocument(), + ); + userEvent.click(screen.getByTestId('modalCloseBtn')); + await waitFor(() => + expect(screen.queryByTestId('modalCloseBtn')).toBeNull(), + ); + }); - // all the action items ordered by earliest first - await waitFor(() => { - expect(screen.getByTestId('sortActionItems')).toHaveTextContent( - translations.earliest, - ); - }); + it('open and closes Item modal (edit)', async () => { + renderOrganizationActionItems(link1); - await waitFor(() => { - expect(screen.getByTestId('selectActionItemStatus')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('selectActionItemStatus')); + const editItemBtn = await screen.findByTestId('editItemBtn1'); + await waitFor(() => expect(editItemBtn).toBeInTheDocument()); + userEvent.click(editItemBtn); - await waitFor(() => { - expect(screen.getByTestId('activeActionItems')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('activeActionItems')); + await waitFor(() => + expect(screen.getAllByText(t.updateActionItem)).toHaveLength(2), + ); + userEvent.click(screen.getByTestId('modalCloseBtn')); + await waitFor(() => + expect(screen.queryByTestId('modalCloseBtn')).toBeNull(), + ); + }); - // all the action items that are active - await waitFor(() => { - expect(screen.getByTestId('selectActionItemStatus')).toHaveTextContent( - translations.active, - ); - }); + it('open and closes Item modal (delete)', async () => { + renderOrganizationActionItems(link1); - await waitFor(() => { - expect(screen.getByTestId('selectActionItemStatus')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('selectActionItemStatus')); + const deleteItemBtn = await screen.findByTestId('deleteItemBtn1'); + expect(deleteItemBtn).toBeInTheDocument(); + userEvent.click(deleteItemBtn); - await waitFor(() => { - expect(screen.getByTestId('completedActionItems')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('completedActionItems')); + await waitFor(() => + expect(screen.getByText(t.deleteActionItem)).toBeInTheDocument(), + ); + userEvent.click(screen.getByTestId('modalCloseBtn')); + await waitFor(() => + expect(screen.queryByTestId('modalCloseBtn')).toBeNull(), + ); + }); - // all the action items that are completed - await waitFor(() => { - expect(screen.getByTestId('selectActionItemStatus')).toHaveTextContent( - translations.completed, - ); - }); + it('open and closes Item modal (update status)', async () => { + renderOrganizationActionItems(link1); - await waitFor(() => { - expect( - screen.getByTestId('selectActionItemCategory'), - ).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('selectActionItemCategory')); + const statusCheckbox = await screen.findByTestId('statusCheckbox1'); + expect(statusCheckbox).toBeInTheDocument(); + userEvent.click(statusCheckbox); - await waitFor(() => { - expect( - screen.getAllByTestId('actionItemCategory')[0], - ).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('actionItemCategory')[0]); + await waitFor(() => + expect(screen.getByText(t.actionItemStatus)).toBeInTheDocument(), + ); + userEvent.click(screen.getByTestId('modalCloseBtn')); + await waitFor(() => + expect(screen.queryByTestId('modalCloseBtn')).toBeNull(), + ); + }); - // action items belonging to this action item category - await waitFor(() => { - expect(screen.getByTestId('selectActionItemCategory')).toHaveTextContent( - 'ActionItemCategory 1', - ); - }); + it('Search action items by assignee', async () => { + renderOrganizationActionItems(link1); - await waitFor(() => { - expect(screen.getByTestId('clearFilters')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('clearFilters')); + const searchByToggle = await screen.findByTestId('searchByToggle'); + expect(searchByToggle).toBeInTheDocument(); - // filters cleared, all the action items belonging to the organization + userEvent.click(searchByToggle); await waitFor(() => { - expect(screen.getByTestId('sortActionItems')).toHaveTextContent( - translations.latest, - ); - expect(screen.getByTestId('selectActionItemStatus')).toHaveTextContent( - translations.status, - ); - expect(screen.getByTestId('selectActionItemCategory')).toHaveTextContent( - translations.actionItemCategory, - ); + expect(screen.getByTestId('assignee')).toBeInTheDocument(); }); - }); - test('opens and closes the create action item modal', async () => { - render( - - - - - - {} - - - - - , - ); + userEvent.click(screen.getByTestId('assignee')); - await wait(); + const searchInput = await screen.findByTestId('searchBy'); + expect(searchInput).toBeInTheDocument(); + userEvent.type(searchInput, 'John'); + userEvent.click(screen.getByTestId('searchBtn')); await waitFor(() => { - expect(screen.getByTestId('createActionItemBtn')).toBeInTheDocument(); + expect(screen.getByText('Category 1')).toBeInTheDocument(); + expect(screen.queryByText('Category 2')).toBeNull(); }); - userEvent.click(screen.getByTestId('createActionItemBtn')); - - await waitFor(() => { - return expect( - screen.findByTestId('createActionItemModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('createActionItemModalCloseBtn')); - - await waitForElementToBeRemoved(() => - screen.queryByTestId('createActionItemModalCloseBtn'), - ); }); - test('creates new action item', async () => { - render( - - - - - - {} - - - - - , - ); + it('Search action items by category', async () => { + renderOrganizationActionItems(link1); - await wait(); + const searchByToggle = await screen.findByTestId('searchByToggle'); + expect(searchByToggle).toBeInTheDocument(); + userEvent.click(searchByToggle); await waitFor(() => { - expect(screen.getByTestId('createActionItemBtn')).toBeInTheDocument(); + expect(screen.getByTestId('category')).toBeInTheDocument(); }); - userEvent.click(screen.getByTestId('createActionItemBtn')); - await waitFor(() => { - return expect( - screen.findByTestId('createActionItemModalCloseBtn'), - ).resolves.toBeInTheDocument(); - }); + userEvent.click(screen.getByTestId('category')); + const searchInput = await screen.findByTestId('searchBy'); + expect(searchInput).toBeInTheDocument(); + + userEvent.type(searchInput, 'Category 1'); + userEvent.click(screen.getByTestId('searchBtn')); await waitFor(() => { - expect( - screen.getByTestId('formSelectActionItemCategory'), - ).toBeInTheDocument(); + expect(screen.getByText('Category 1')).toBeInTheDocument(); + expect(screen.queryByText('Category 2')).toBeNull(); }); + }); - userEvent.selectOptions( - screen.getByTestId('formSelectActionItemCategory'), - formData.actionItemCategory, - ); - - userEvent.selectOptions( - screen.getByTestId('formSelectAssignee'), - formData.assignee, - ); - - userEvent.type( - screen.getByPlaceholderText(translations.preCompletionNotes), - formData.preCompletionNotes, - ); - - const dueDatePicker = screen.getByLabelText(translations.dueDate); - fireEvent.change(dueDatePicker, { - target: { value: formData.dueDate }, - }); + it('Search action items by name and clear the input by backspace', async () => { + renderOrganizationActionItems(link1); - userEvent.click(screen.getByTestId('createActionItemFormSubmitBtn')); + const searchInput = await screen.findByTestId('searchBy'); + expect(searchInput).toBeInTheDocument(); + // Clear the search input by backspace + userEvent.type(searchInput, 'A{backspace}'); await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.successfulCreation); + expect(screen.getByText('Category 1')).toBeInTheDocument(); + expect(screen.getByText('Category 2')).toBeInTheDocument(); }); }); - test('toasts error on unsuccessful creation', async () => { - render( - - - - - - {} - - - - - , - ); - - await wait(); + it('Search action items by name on press of ENTER', async () => { + renderOrganizationActionItems(link1); - await waitFor(() => { - expect(screen.getByTestId('createActionItemBtn')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('createActionItemBtn')); + const searchInput = await screen.findByTestId('searchBy'); + expect(searchInput).toBeInTheDocument(); + userEvent.type(searchInput, 'John'); + userEvent.type(searchInput, '{enter}'); await waitFor(() => { - return expect( - screen.findByTestId('createActionItemModalCloseBtn'), - ).resolves.toBeInTheDocument(); + expect(screen.getByText('Category 1')).toBeInTheDocument(); + expect(screen.queryByText('Category 2')).toBeNull(); }); + }); + it('should render Empty Action Item Categories Screen', async () => { + renderOrganizationActionItems(link3); await waitFor(() => { - expect( - screen.getByTestId('formSelectActionItemCategory'), - ).toBeInTheDocument(); - }); - - userEvent.selectOptions( - screen.getByTestId('formSelectActionItemCategory'), - formData.actionItemCategory, - ); - - userEvent.selectOptions( - screen.getByTestId('formSelectAssignee'), - formData.assignee, - ); - - userEvent.type( - screen.getByPlaceholderText(translations.preCompletionNotes), - formData.preCompletionNotes, - ); - - const dueDatePicker = screen.getByLabelText(translations.dueDate); - fireEvent.change(dueDatePicker, { - target: { value: formData.dueDate }, + expect(screen.getByTestId('searchBy')).toBeInTheDocument(); + expect(screen.getByText(t.noActionItems)).toBeInTheDocument(); }); + }); - userEvent.click(screen.getByTestId('createActionItemFormSubmitBtn')); - + it('should render the Action Item Categories Screen with error', async () => { + renderOrganizationActionItems(link2); await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); + expect(screen.getByTestId('errorMsg')).toBeInTheDocument(); }); }); - - test('Testing Only Action Items Displaying', async () => { - const mockApp = render( - - - - - - {} - - - - - , - ); - - await waitFor(mockApp.asFragment); - - const actionItem = screen.getByText(/John Doe/i); - - expect(actionItem).toContainHTML('John Doe'); - }); }); diff --git a/src/screens/OrganizationActionItems/OrganizationActionItems.tsx b/src/screens/OrganizationActionItems/OrganizationActionItems.tsx index c75460cf43..ba48f212e0 100644 --- a/src/screens/OrganizationActionItems/OrganizationActionItems.tsx +++ b/src/screens/OrganizationActionItems/OrganizationActionItems.tsx @@ -1,31 +1,71 @@ -import React, { useState } from 'react'; -import type { ChangeEvent } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Dropdown } from 'react-bootstrap'; -import { useParams } from 'react-router-dom'; +import { Button, Dropdown, Form } from 'react-bootstrap'; +import { Navigate, useParams } from 'react-router-dom'; -import SortIcon from '@mui/icons-material/Sort'; -import { WarningAmberRounded } from '@mui/icons-material'; +import { + Circle, + FilterAltOutlined, + Search, + Sort, + WarningAmberRounded, +} from '@mui/icons-material'; import dayjs from 'dayjs'; -import { toast } from 'react-toastify'; -import { useMutation, useQuery } from '@apollo/client'; -import { - ACTION_ITEM_CATEGORY_LIST, - ACTION_ITEM_LIST, - MEMBERS_LIST, -} from 'GraphQl/Queries/Queries'; -import { CREATE_ACTION_ITEM_MUTATION } from 'GraphQl/Mutations/mutations'; +import { useQuery } from '@apollo/client'; +import { ACTION_ITEM_LIST } from 'GraphQl/Queries/Queries'; import type { - InterfaceActionItemCategoryList, + InterfaceActionItemInfo, InterfaceActionItemList, - InterfaceMembersList, } from 'utils/interfaces'; -import ActionItemsContainer from 'components/ActionItems/ActionItemsContainer'; -import ActionItemCreateModal from './ActionItemCreateModal'; import styles from './OrganizationActionItems.module.css'; import Loader from 'components/Loader/Loader'; +import { + DataGrid, + type GridCellParams, + type GridColDef, +} from '@mui/x-data-grid'; +import { Chip, Stack } from '@mui/material'; +import ItemViewModal from './ItemViewModal'; +import ItemModal from './ItemModal'; +import ItemDeleteModal from './ItemDeleteModal'; +import Avatar from 'components/Avatar/Avatar'; +import ItemUpdateStatusModal from './ItemUpdateStatusModal'; + +enum ItemStatus { + Pending = 'pending', + Completed = 'completed', + Late = 'late', +} + +enum ModalState { + SAME = 'same', + DELETE = 'delete', + VIEW = 'view', + STATUS = 'status', +} + +const dataGridStyle = { + '&.MuiDataGrid-root .MuiDataGrid-cell:focus-within': { + outline: 'none !important', + }, + '&.MuiDataGrid-root .MuiDataGrid-columnHeader:focus-within': { + outline: 'none', + }, + '& .MuiDataGrid-row:hover': { + backgroundColor: 'transparent', + }, + '& .MuiDataGrid-row.Mui-hovered': { + backgroundColor: 'transparent', + }, + '& .MuiDataGrid-root': { + borderRadius: '0.5rem', + }, + '& .MuiDataGrid-main': { + borderRadius: '0.5rem', + }, +}; /** * Component for managing and displaying action items within an organization. @@ -39,57 +79,51 @@ function organizationActionItems(): JSX.Element { keyPrefix: 'organizationActionItems', }); const { t: tCommon } = useTranslation('common'); + const { t: tErrors } = useTranslation('errors'); // Get the organization ID from URL parameters - const { orgId: currentUrl } = useParams(); + const { orgId, eventId } = useParams(); - // State for managing modal visibility and form data - const [actionItemCreateModalIsOpen, setActionItemCreateModalIsOpen] = - useState(false); - const [dueDate, setDueDate] = useState(new Date()); - const [orderBy, setOrderBy] = useState<'Latest' | 'Earliest'>('Latest'); - const [actionItemStatus, setActionItemStatus] = useState(''); - const [actionItemCategoryId, setActionItemCategoryId] = useState(''); - const [actionItemCategoryName, setActionItemCategoryName] = useState(''); + if (!orgId) { + return ; + } - const [formState, setFormState] = useState({ - actionItemCategoryId: '', - assigneeId: '', - preCompletionNotes: '', + const [actionItem, setActionItem] = useState( + null, + ); + const [modalMode, setModalMode] = useState<'create' | 'edit'>('create'); + const [searchValue, setSearchValue] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); + const [sortBy, setSortBy] = useState<'dueDate_ASC' | 'dueDate_DESC' | null>( + null, + ); + const [status, setStatus] = useState(null); + const [searchBy, setSearchBy] = useState<'assignee' | 'category'>('assignee'); + const [modalState, setModalState] = useState<{ + [key in ModalState]: boolean; + }>({ + [ModalState.SAME]: false, + [ModalState.DELETE]: false, + [ModalState.VIEW]: false, + [ModalState.STATUS]: false, }); - /** - * Query to fetch action item categories for the organization. - */ - const { - data: actionItemCategoriesData, - loading: actionItemCategoriesLoading, - error: actionItemCategoriesError, - }: { - data: InterfaceActionItemCategoryList | undefined; - loading: boolean; - error?: Error | undefined; - } = useQuery(ACTION_ITEM_CATEGORY_LIST, { - variables: { - organizationId: currentUrl, - }, - notifyOnNetworkStatusChange: true, - }); + const openModal = (modal: ModalState): void => + setModalState((prevState) => ({ ...prevState, [modal]: true })); - /** - * Query to fetch members of the organization. - */ - const { - data: membersData, - loading: membersLoading, - error: membersError, - }: { - data: InterfaceMembersList | undefined; - loading: boolean; - error?: Error | undefined; - } = useQuery(MEMBERS_LIST, { - variables: { id: currentUrl }, - }); + const closeModal = (modal: ModalState): void => + setModalState((prevState) => ({ ...prevState, [modal]: false })); + + const handleModalClick = useCallback( + (actionItem: InterfaceActionItemInfo | null, modal: ModalState): void => { + if (modal === ModalState.SAME) { + setModalMode(actionItem ? 'edit' : 'create'); + } + setActionItem(actionItem); + openModal(modal); + }, + [openModal], + ); /** * Query to fetch action items for the organization based on filters and sorting. @@ -106,335 +140,407 @@ function organizationActionItems(): JSX.Element { refetch: () => void; } = useQuery(ACTION_ITEM_LIST, { variables: { - organizationId: currentUrl, - actionItemCategoryId, - orderBy: orderBy === 'Latest' ? 'createdAt_DESC' : 'createdAt_ASC', - isActive: actionItemStatus === 'Active' ? true : false, - isCompleted: actionItemStatus === 'Completed' ? true : false, + organizationId: orgId, + eventId: eventId, + orderBy: sortBy, + where: { + assigneeName: searchBy === 'assignee' ? searchTerm : undefined, + categoryName: searchBy === 'category' ? searchTerm : undefined, + is_completed: + status === null ? undefined : status === ItemStatus.Completed, + }, }, - notifyOnNetworkStatusChange: true, }); - /** - * Mutation to create a new action item. - */ - const [createActionItem] = useMutation(CREATE_ACTION_ITEM_MUTATION); - - /** - * Handler function to create a new action item. - * - * @param e - The form submit event. - * @returns A promise that resolves when the action item is created. - */ - const createActionItemHandler = async ( - e: ChangeEvent, - ): Promise => { - e.preventDefault(); - try { - await createActionItem({ - variables: { - assigneeId: formState.assigneeId, - actionItemCategoryId: formState.actionItemCategoryId, - preCompletionNotes: formState.preCompletionNotes, - dueDate: dayjs(dueDate).format('YYYY-MM-DD'), - }, - }); - - // Reset form and date after successful creation - setFormState({ - assigneeId: '', - actionItemCategoryId: '', - preCompletionNotes: '', - }); - - setDueDate(new Date()); - - actionItemsRefetch(); - hideCreateModal(); - toast.success(t('successfulCreation') as string); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - console.log(error.message); - } - } - }; - - /** - * Toggles the visibility of the create action item modal. - */ - const showCreateModal = (): void => { - setActionItemCreateModalIsOpen(!actionItemCreateModalIsOpen); - }; - - /** - * Hides the create action item modal. - */ - const hideCreateModal = (): void => { - setActionItemCreateModalIsOpen(!actionItemCreateModalIsOpen); - }; - - /** - * Handles sorting action items by date. - * - * @param sort - The sorting order ('Latest' or 'Earliest'). - */ - const handleSorting = (sort: string): void => { - if (sort === 'Latest') { - setOrderBy('Latest'); - } else { - setOrderBy('Earliest'); - } - }; - - /** - * Filters action items by status. - * - * @param status - The status to filter by ('Active' or 'Completed'). - */ - const handleStatusFilter = (status: string): void => { - if (status === 'Active') { - setActionItemStatus('Active'); - } else { - setActionItemStatus('Completed'); - } - }; - - /** - * Clears all filters applied to the action items. - */ - const handleClearFilters = (): void => { - setActionItemCategoryId(''); - setActionItemCategoryName(''); - setActionItemStatus(''); - setOrderBy('Latest'); - }; + const actionItems = useMemo( + () => actionItemsData?.actionItemsByOrganization || [], + [actionItemsData], + ); - if (actionItemCategoriesLoading || membersLoading || actionItemsLoading) { + if (actionItemsLoading) { return ; } - if (actionItemCategoriesError || membersError || actionItemsError) { + if (actionItemsError) { return ( -
-
- -
- Error occured while loading{' '} - {actionItemCategoriesError - ? 'Action Item Categories' - : membersError - ? 'Members List' - : 'Action Items List'}{' '} - Data -
- {actionItemCategoriesError - ? actionItemCategoriesError.message - : membersError - ? membersError.message - : actionItemsError?.message} -
-
+
+ +
+ {tErrors('errorLoading', { entity: 'Action Items' })} +
+ {`${actionItemsError.message}`} +
); } - const actionItemCategories = - actionItemCategoriesData?.actionItemCategoriesByOrganization.filter( - (category) => !category.isDisabled, - ); - - const actionItemOnly = actionItemsData?.actionItemsByOrganization.filter( - (item) => item.event == null, - ); + const columns: GridColDef[] = [ + { + field: 'assignee', + headerName: 'Assignee', + flex: 1, + align: 'left', + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + renderCell: (params: GridCellParams) => { + const { _id, firstName, lastName, image } = params.row.assignee; + return ( +
+ {image ? ( + Assignee + ) : ( +
+ +
+ )} + {params.row.assignee.firstName + ' ' + params.row.assignee.lastName} +
+ ); + }, + }, + { + field: 'itemCategory', + headerName: 'Item Category', + flex: 1, + align: 'center', + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + renderCell: (params: GridCellParams) => { + return ( +
+ {params.row.actionItemCategory?.name} +
+ ); + }, + }, + { + field: 'status', + headerName: 'Status', + flex: 1, + align: 'center', + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + renderCell: (params: GridCellParams) => { + return ( + } + label={params.row.isCompleted ? 'Completed' : 'Pending'} + variant="outlined" + color="primary" + className={`${styles.chip} ${params.row.isCompleted ? styles.active : styles.pending}`} + /> + ); + }, + }, + { + field: 'allotedHours', + headerName: 'Alloted Hours', + align: 'center', + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + flex: 1, + renderCell: (params: GridCellParams) => { + return ( +
{params.row.allotedHours ?? '-'}
+ ); + }, + }, + { + field: 'dueDate', + headerName: 'Due Date', + align: 'center', + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + flex: 1, + renderCell: (params: GridCellParams) => { + return ( +
+ {dayjs(params.row.dueDate).format('DD/MM/YYYY')} +
+ ); + }, + }, + { + field: 'options', + headerName: 'Options', + align: 'center', + flex: 1, + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + renderCell: (params: GridCellParams) => { + return ( + <> + + + + + ); + }, + }, + { + field: 'completed', + headerName: 'Completed', + align: 'center', + flex: 1, + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + renderCell: (params: GridCellParams) => { + return ( +
+ handleModalClick(params.row, ModalState.STATUS)} + /> +
+ ); + }, + }, + ]; return ( -
-
-
-
-
-
-
+ {/* Table with Action Items */} + row._id} + slots={{ + noRowsOverlay: () => ( + + {t('noActionItems')} + + ), + }} + sx={dataGridStyle} + getRowClassName={() => `${styles.rowBackground}`} + autoHeight + rowHeight={65} + rows={actionItems.map((actionItem, index) => ({ + id: index + 1, + ...actionItem, + }))} + columns={columns} + isRowSelectable={() => false} + /> - -
+ {/* Item Modal (Create/Edit) */} + closeModal(ModalState.SAME)} + orgId={orgId} + actionItemsRefetch={actionItemsRefetch} + actionItem={actionItem} + editMode={modalMode === 'edit'} + /> + + closeModal(ModalState.DELETE)} + actionItem={actionItem} + actionItemsRefetch={actionItemsRefetch} + /> - {/* Create Modal */} - closeModal(ModalState.STATUS)} + actionItemsRefetch={actionItemsRefetch} /> + + {/* View Modal */} + {actionItem && ( + closeModal(ModalState.VIEW)} + item={actionItem} + /> + )}
); } diff --git a/src/screens/OrganizationActionItems/OrganizationActionItemsErrorMocks.ts b/src/screens/OrganizationActionItems/OrganizationActionItemsErrorMocks.ts deleted file mode 100644 index bedba6572b..0000000000 --- a/src/screens/OrganizationActionItems/OrganizationActionItemsErrorMocks.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { CREATE_ACTION_ITEM_MUTATION } from 'GraphQl/Mutations/mutations'; - -import { - ACTION_ITEM_CATEGORY_LIST, - ACTION_ITEM_LIST, - MEMBERS_LIST, -} from 'GraphQl/Queries/Queries'; - -export const MOCKS_ERROR_ACTION_ITEM_CATEGORY_LIST_QUERY = [ - { - request: { - query: ACTION_ITEM_CATEGORY_LIST, - variables: { organizationId: '123' }, - }, - error: new Error('Mock Graphql Error'), - }, -]; - -export const MOCKS_ERROR_MEMBERS_LIST_QUERY = [ - { - request: { - query: ACTION_ITEM_CATEGORY_LIST, - variables: { organizationId: '123' }, - }, - result: { - data: { - actionItemCategoriesByOrganization: [ - { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - isDisabled: false, - }, - ], - }, - }, - }, - { - request: { - query: MEMBERS_LIST, - variables: { id: '123' }, - }, - error: new Error('Mock Graphql Error'), - }, -]; - -export const MOCKS_ERROR_ACTION_ITEM_LIST_QUERY = [ - { - request: { - query: ACTION_ITEM_CATEGORY_LIST, - variables: { organizationId: '123' }, - }, - result: { - data: { - actionItemCategoriesByOrganization: [ - { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - isDisabled: false, - }, - ], - }, - }, - }, - { - request: { - query: MEMBERS_LIST, - variables: { id: '123' }, - }, - result: { - data: { - organizations: [ - { - _id: '123', - members: [ - { - _id: 'user1', - firstName: 'Harve', - lastName: 'Lance', - email: 'harve@example.com', - image: '', - organizationsBlockedBy: [], - createdAt: '2024-02-14', - }, - ], - }, - ], - }, - }, - }, - { - request: { - query: ACTION_ITEM_LIST, - variables: { id: '123' }, - }, - error: new Error('Mock Graphql Error'), - }, -]; - -export const MOCKS_ERROR_MUTATIONS = [ - { - request: { - query: ACTION_ITEM_CATEGORY_LIST, - variables: { organizationId: '123' }, - }, - result: { - data: { - actionItemCategoriesByOrganization: [ - { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - isDisabled: false, - }, - ], - }, - }, - }, - { - request: { - query: MEMBERS_LIST, - variables: { id: '123' }, - }, - result: { - data: { - organizations: [ - { - _id: '123', - members: [ - { - _id: 'user1', - firstName: 'Harve', - lastName: 'Lance', - email: 'harve@example.com', - image: '', - organizationsBlockedBy: [], - createdAt: '2024-02-14', - }, - ], - }, - ], - }, - }, - }, - { - request: { - query: ACTION_ITEM_LIST, - variables: { - organizationId: '123', - orderBy: 'createdAt_DESC', - actionItemCategoryId: '', - isActive: false, - isCompleted: false, - }, - }, - result: { - data: { - actionItemsByOrganization: [ - { - _id: 'actionItem1', - assignee: { - _id: 'user1', - firstName: 'Harve', - lastName: 'Lance', - }, - actionItemCategory: { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - }, - preCompletionNotes: 'Pre Completion Notes', - postCompletionNotes: 'Post Completion Notes', - assignmentDate: '2024-02-14', - dueDate: '2024-02-21', - completionDate: '2024-02-21', - isCompleted: false, - assigner: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - _id: 'event1', - title: 'event 1', - }, - creator: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - }, - ], - }, - }, - }, - { - request: { - query: ACTION_ITEM_LIST, - variables: { - organizationId: '123', - eventId: 'event1', - orderBy: 'createdAt_DESC', - }, - }, - result: { - data: { - actionItemsByOrganization: [ - { - _id: 'actionItem1', - assignee: { - _id: 'user1', - firstName: 'Harve', - lastName: 'Lance', - }, - actionItemCategory: { - _id: 'actionItemCategory1', - name: 'ActionItemCategory 1', - }, - preCompletionNotes: 'Pre Completion Notes', - postCompletionNotes: 'Post Completion Notes', - assignmentDate: '2024-02-14', - dueDate: '2024-02-21', - completionDate: '2024-02-21', - isCompleted: false, - assigner: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - event: { - _id: 'event1', - title: 'event 1', - }, - creator: { - _id: 'user0', - firstName: 'Wilt', - lastName: 'Shepherd', - }, - }, - ], - }, - }, - }, - { - request: { - query: CREATE_ACTION_ITEM_MUTATION, - variables: { - actionItemCategoryId: 'actionItemCategory1', - assigneeId: 'user1', - preCompletionNotes: 'pre completion notes', - dueDate: '2024-02-14', - }, - }, - error: new Error('Mock Graphql Error'), - }, - { - request: { - query: CREATE_ACTION_ITEM_MUTATION, - variables: { - eventId: 'event1', - actionItemCategoryId: 'actionItemCategory1', - assigneeId: 'user1', - preCompletionNotes: 'pre completion notes', - dueDate: '2024-02-14', - }, - }, - error: new Error('Mock Graphql Error'), - }, -]; diff --git a/src/screens/UserPortal/Campaigns/Campaigns.test.tsx b/src/screens/UserPortal/Campaigns/Campaigns.test.tsx index 443d643cff..17b7eec4d5 100644 --- a/src/screens/UserPortal/Campaigns/Campaigns.test.tsx +++ b/src/screens/UserPortal/Campaigns/Campaigns.test.tsx @@ -155,14 +155,17 @@ describe('Testing User Campaigns Screen', () => { it('Check if All details are rendered correctly', async () => { renderCampaigns(link1); + + const detailContainer = await screen.findByTestId('detailContainer1'); + const detailContainer2 = await screen.findByTestId('detailContainer2'); await waitFor(() => { - const detailContainer = screen.getByTestId('detailContainer1'); + expect(detailContainer).toBeInTheDocument(); + expect(detailContainer2).toBeInTheDocument(); expect(detailContainer).toHaveTextContent('School Campaign'); expect(detailContainer).toHaveTextContent('$22000'); expect(detailContainer).toHaveTextContent('2024-07-28'); expect(detailContainer).toHaveTextContent('2025-08-31'); expect(detailContainer).toHaveTextContent('Active'); - const detailContainer2 = screen.getByTestId('detailContainer2'); expect(detailContainer2).toHaveTextContent('Hospital Campaign'); expect(detailContainer2).toHaveTextContent('$9000'); expect(detailContainer2).toHaveTextContent('2024-07-28'); @@ -291,18 +294,6 @@ describe('Testing User Campaigns Screen', () => { }); }); - it('Redirect to My Pledges screen', async () => { - renderCampaigns(link1); - - const myPledgesBtn = await screen.findByText(cTranslations.myPledges); - expect(myPledgesBtn).toBeInTheDocument(); - userEvent.click(myPledgesBtn); - - await waitFor(() => { - expect(screen.getByTestId('pledgeScreen')).toBeInTheDocument(); - }); - }); - it('open and closes add pledge modal', async () => { renderCampaigns(link1); @@ -318,4 +309,16 @@ describe('Testing User Campaigns Screen', () => { expect(screen.queryByTestId('pledgeModalCloseBtn')).toBeNull(), ); }); + + it('Redirect to My Pledges screen', async () => { + renderCampaigns(link1); + + const myPledgesBtn = await screen.findByText(cTranslations.myPledges); + expect(myPledgesBtn).toBeInTheDocument(); + userEvent.click(myPledgesBtn); + + await waitFor(() => { + expect(screen.getByTestId('pledgeScreen')).toBeInTheDocument(); + }); + }); }); diff --git a/src/screens/UserPortal/Campaigns/CampaignsMocks.ts b/src/screens/UserPortal/Campaigns/CampaignsMocks.ts index 7b91fac025..f64401bca5 100644 --- a/src/screens/UserPortal/Campaigns/CampaignsMocks.ts +++ b/src/screens/UserPortal/Campaigns/CampaignsMocks.ts @@ -1,6 +1,63 @@ import { USER_DETAILS } from 'GraphQl/Queries/Queries'; import { USER_FUND_CAMPAIGNS } from 'GraphQl/Queries/fundQueries'; +const userDetailsQuery = { + request: { + query: USER_DETAILS, + variables: { + id: 'userId', + }, + }, + result: { + data: { + user: { + user: { + _id: 'userId', + joinedOrganizations: [ + { + _id: '6537904485008f171cf29924', + __typename: 'Organization', + }, + ], + firstName: 'Harve', + lastName: 'Lance', + email: 'testuser1@example.com', + image: null, + createdAt: '2023-04-13T04:53:17.742Z', + birthDate: null, + educationGrade: null, + employmentStatus: null, + gender: null, + maritalStatus: null, + phone: null, + address: { + line1: 'Line1', + countryCode: 'CountryCode', + city: 'CityName', + state: 'State', + __typename: 'Address', + }, + registeredEvents: [], + membershipRequests: [], + __typename: 'User', + }, + appUserProfile: { + _id: '67078abd85008f171cf2991d', + adminFor: [], + isSuperAdmin: false, + appLanguageCode: 'en', + pluginCreationAllowed: true, + createdOrganizations: [], + createdEvents: [], + eventAdmin: [], + __typename: 'AppUserProfile', + }, + __typename: 'UserData', + }, + }, + }, +}; + export const MOCKS = [ { request: { @@ -173,62 +230,7 @@ export const MOCKS = [ }, }, }, - { - request: { - query: USER_DETAILS, - variables: { - id: 'userId', - }, - }, - result: { - data: { - user: { - user: { - _id: 'userId', - joinedOrganizations: [ - { - _id: '6537904485008f171cf29924', - __typename: 'Organization', - }, - ], - firstName: 'Harve', - lastName: 'Lance', - email: 'testuser1@example.com', - image: null, - createdAt: '2023-04-13T04:53:17.742Z', - birthDate: null, - educationGrade: null, - employmentStatus: null, - gender: null, - maritalStatus: null, - phone: null, - address: { - line1: 'Line1', - countryCode: 'CountryCode', - city: 'CityName', - state: 'State', - __typename: 'Address', - }, - registeredEvents: [], - membershipRequests: [], - __typename: 'User', - }, - appUserProfile: { - _id: '67078abd85008f171cf2991d', - adminFor: [], - isSuperAdmin: false, - appLanguageCode: 'en', - pluginCreationAllowed: true, - createdOrganizations: [], - createdEvents: [], - eventAdmin: [], - __typename: 'AppUserProfile', - }, - __typename: 'UserData', - }, - }, - }, - }, + userDetailsQuery, ]; export const EMPTY_MOCKS = [ @@ -249,62 +251,7 @@ export const EMPTY_MOCKS = [ }, }, }, - { - request: { - query: USER_DETAILS, - variables: { - id: 'userId', - }, - }, - result: { - data: { - user: { - user: { - _id: 'userId', - joinedOrganizations: [ - { - _id: '6537904485008f171cf29924', - __typename: 'Organization', - }, - ], - firstName: 'Harve', - lastName: 'Lance', - email: 'testuser1@example.com', - image: null, - createdAt: '2023-04-13T04:53:17.742Z', - birthDate: null, - educationGrade: null, - employmentStatus: null, - gender: null, - maritalStatus: null, - phone: null, - address: { - line1: 'Line1', - countryCode: 'CountryCode', - city: 'CityName', - state: 'State', - __typename: 'Address', - }, - registeredEvents: [], - membershipRequests: [], - __typename: 'User', - }, - appUserProfile: { - _id: '67078abd85008f171cf2991d', - adminFor: [], - isSuperAdmin: false, - appLanguageCode: 'en', - pluginCreationAllowed: true, - createdOrganizations: [], - createdEvents: [], - eventAdmin: [], - __typename: 'AppUserProfile', - }, - __typename: 'UserData', - }, - }, - }, - }, + userDetailsQuery, ]; export const USER_FUND_CAMPAIGNS_ERROR = [ @@ -321,4 +268,5 @@ export const USER_FUND_CAMPAIGNS_ERROR = [ }, error: new Error('Error fetching campaigns'), }, + userDetailsQuery, ]; diff --git a/src/screens/UserPortal/Pledges/Pledge.test.tsx b/src/screens/UserPortal/Pledges/Pledge.test.tsx index ecdd25a1d3..3d5eef94c2 100644 --- a/src/screens/UserPortal/Pledges/Pledge.test.tsx +++ b/src/screens/UserPortal/Pledges/Pledge.test.tsx @@ -126,20 +126,6 @@ describe('Testing User Pledge Screen', () => { }); }); - it('should render the Campaign Pledge screen with error', async () => { - renderMyPledges(link2); - await waitFor(() => { - expect(screen.getByTestId('errorMsg')).toBeInTheDocument(); - }); - }); - - it('renders the empty pledge component', async () => { - renderMyPledges(link3); - await waitFor(() => - expect(screen.getByText(translations.noPledges)).toBeInTheDocument(), - ); - }); - it('check if user image renders', async () => { renderMyPledges(link1); await waitFor(() => { @@ -352,4 +338,18 @@ describe('Testing User Pledge Screen', () => { expect(screen.queryByTestId('pledgeModalCloseBtn')).toBeNull(), ); }); + + it('should render the Campaign Pledge screen with error', async () => { + renderMyPledges(link2); + await waitFor(() => { + expect(screen.getByTestId('errorMsg')).toBeInTheDocument(); + }); + }); + + it('renders the empty pledge component', async () => { + renderMyPledges(link3); + await waitFor(() => + expect(screen.getByText(translations.noPledges)).toBeInTheDocument(), + ); + }); }); diff --git a/src/screens/UserPortal/Pledges/PledgesMocks.ts b/src/screens/UserPortal/Pledges/PledgesMocks.ts index 9aa3780fbd..c7666987ff 100644 --- a/src/screens/UserPortal/Pledges/PledgesMocks.ts +++ b/src/screens/UserPortal/Pledges/PledgesMocks.ts @@ -1,11 +1,64 @@ -import { - CREATE_PlEDGE, - DELETE_PLEDGE, - UPDATE_PLEDGE, -} from 'GraphQl/Mutations/PledgeMutation'; +import { DELETE_PLEDGE } from 'GraphQl/Mutations/PledgeMutation'; import { USER_DETAILS } from 'GraphQl/Queries/Queries'; import { USER_PLEDGES } from 'GraphQl/Queries/fundQueries'; +const userDetailsQuery = { + request: { + query: USER_DETAILS, + variables: { + id: 'userId', + }, + }, + result: { + data: { + user: { + user: { + _id: 'userId', + joinedOrganizations: [ + { + _id: '6537904485008f171cf29924', + __typename: 'Organization', + }, + ], + firstName: 'Harve', + lastName: 'Lance', + email: 'testuser1@example.com', + image: null, + createdAt: '2023-04-13T04:53:17.742Z', + birthDate: null, + educationGrade: null, + employmentStatus: null, + gender: null, + maritalStatus: null, + phone: null, + address: { + line1: 'Line1', + countryCode: 'CountryCode', + city: 'CityName', + state: 'State', + __typename: 'Address', + }, + registeredEvents: [], + membershipRequests: [], + __typename: 'User', + }, + appUserProfile: { + _id: '67078abd85008f171cf2991d', + adminFor: [], + isSuperAdmin: false, + appLanguageCode: 'en', + pluginCreationAllowed: true, + createdOrganizations: [], + createdEvents: [], + eventAdmin: [], + __typename: 'AppUserProfile', + }, + __typename: 'UserData', + }, + }, + }, +}; + export const MOCKS = [ { request: { @@ -501,62 +554,7 @@ export const MOCKS = [ }, }, }, - { - request: { - query: USER_DETAILS, - variables: { - id: 'userId', - }, - }, - result: { - data: { - user: { - user: { - _id: 'userId', - joinedOrganizations: [ - { - _id: '6537904485008f171cf29924', - __typename: 'Organization', - }, - ], - firstName: 'Harve', - lastName: 'Lance', - email: 'testuser1@example.com', - image: null, - createdAt: '2023-04-13T04:53:17.742Z', - birthDate: null, - educationGrade: null, - employmentStatus: null, - gender: null, - maritalStatus: null, - phone: null, - address: { - line1: 'Line1', - countryCode: 'CountryCode', - city: 'CityName', - state: 'State', - __typename: 'Address', - }, - registeredEvents: [], - membershipRequests: [], - __typename: 'User', - }, - appUserProfile: { - _id: '67078abd85008f171cf2991d', - adminFor: [], - isSuperAdmin: false, - appLanguageCode: 'en', - pluginCreationAllowed: true, - createdOrganizations: [], - createdEvents: [], - eventAdmin: [], - __typename: 'AppUserProfile', - }, - __typename: 'UserData', - }, - }, - }, - }, + userDetailsQuery, ]; export const EMPTY_MOCKS = [ @@ -577,6 +575,7 @@ export const EMPTY_MOCKS = [ }, }, }, + userDetailsQuery, ]; export const USER_PLEDGES_ERROR = [ @@ -593,4 +592,5 @@ export const USER_PLEDGES_ERROR = [ }, error: new Error('Error fetching pledges'), }, + userDetailsQuery, ]; diff --git a/src/state/reducers/routesReducer.test.ts b/src/state/reducers/routesReducer.test.ts index 8d8de8a5dd..8bdc1c069b 100644 --- a/src/state/reducers/routesReducer.test.ts +++ b/src/state/reducers/routesReducer.test.ts @@ -17,7 +17,6 @@ describe('Testing Routes reducer', () => { { name: 'Events', url: '/orgevents/undefined' }, { name: 'Venues', url: '/orgvenues/undefined' }, { name: 'Action Items', url: '/orgactionitems/undefined' }, - { name: 'Agenda Items Category', url: '/orgagendacategory/undefined' }, { name: 'Posts', url: '/orgpost/undefined' }, { name: 'Block/Unblock', @@ -70,11 +69,6 @@ describe('Testing Routes reducer', () => { comp_id: 'orgactionitems', component: 'OrganizationActionItems', }, - { - name: 'Agenda Items Category', - comp_id: 'orgagendacategory', - component: 'OrganizationAgendaCategory', - }, { name: 'Posts', comp_id: 'orgpost', component: 'OrgPost' }, { name: 'Block/Unblock', comp_id: 'blockuser', component: 'BlockUser' }, { @@ -126,7 +120,6 @@ describe('Testing Routes reducer', () => { { name: 'Events', url: '/orgevents/orgId' }, { name: 'Venues', url: '/orgvenues/orgId' }, { name: 'Action Items', url: '/orgactionitems/orgId' }, - { name: 'Agenda Items Category', url: '/orgagendacategory/orgId' }, { name: 'Posts', url: '/orgpost/orgId' }, { name: 'Block/Unblock', url: '/blockuser/orgId' }, { name: 'Advertisement', url: '/orgads/orgId' }, @@ -176,11 +169,6 @@ describe('Testing Routes reducer', () => { comp_id: 'orgactionitems', component: 'OrganizationActionItems', }, - { - name: 'Agenda Items Category', - comp_id: 'orgagendacategory', - component: 'OrganizationAgendaCategory', - }, { name: 'Posts', comp_id: 'orgpost', component: 'OrgPost' }, { name: 'Block/Unblock', comp_id: 'blockuser', component: 'BlockUser' }, { @@ -228,7 +216,6 @@ describe('Testing Routes reducer', () => { { name: 'Events', url: '/orgevents/undefined' }, { name: 'Venues', url: '/orgvenues/undefined' }, { name: 'Action Items', url: '/orgactionitems/undefined' }, - { name: 'Agenda Items Category', url: '/orgagendacategory/undefined' }, { name: 'Posts', url: '/orgpost/undefined' }, { name: 'Block/Unblock', @@ -284,11 +271,6 @@ describe('Testing Routes reducer', () => { comp_id: 'orgactionitems', component: 'OrganizationActionItems', }, - { - name: 'Agenda Items Category', - comp_id: 'orgagendacategory', - component: 'OrganizationAgendaCategory', - }, { name: 'Posts', comp_id: 'orgpost', component: 'OrgPost' }, { name: 'Block/Unblock', comp_id: 'blockuser', component: 'BlockUser' }, { diff --git a/src/state/reducers/routesReducer.ts b/src/state/reducers/routesReducer.ts index 878fe73099..5d50f5402d 100644 --- a/src/state/reducers/routesReducer.ts +++ b/src/state/reducers/routesReducer.ts @@ -77,11 +77,6 @@ const components: ComponentType[] = [ comp_id: 'orgactionitems', component: 'OrganizationActionItems', }, - { - name: 'Agenda Items Category', - comp_id: 'orgagendacategory', - component: 'OrganizationAgendaCategory', - }, { name: 'Posts', comp_id: 'orgpost', component: 'OrgPost' }, { name: 'Block/Unblock', comp_id: 'blockuser', component: 'BlockUser' }, { name: 'Advertisement', comp_id: 'orgads', component: 'Advertisements' }, diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 99267d239c..1cec1e739b 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -11,6 +11,8 @@ export interface InterfaceActionItemCategoryInfo { _id: string; name: string; isDisabled: boolean; + createdAt: string; + creator: { _id: string; firstName: string; lastName: string }; } export interface InterfaceActionItemCategoryList { @@ -23,18 +25,20 @@ export interface InterfaceActionItemInfo { _id: string; firstName: string; lastName: string; + image: string | null; }; assigner: { _id: string; firstName: string; lastName: string; + image: string | null; }; actionItemCategory: { _id: string; name: string; }; preCompletionNotes: string; - postCompletionNotes: string; + postCompletionNotes: string | null; assignmentDate: Date; dueDate: Date; completionDate: Date; @@ -42,12 +46,13 @@ export interface InterfaceActionItemInfo { event: { _id: string; title: string; - }; + } | null; creator: { _id: string; firstName: string; lastName: string; }; + allotedHours: number | null; } export interface InterfaceActionItemList { @@ -556,3 +561,8 @@ export interface InterfaceAgendaItemList { export interface InterfaceMapType { [key: string]: string; } + +export interface InterfaceCustomFieldData { + type: string; + name: string; +}