-
-
- {tErrors('errorLoading', { entity: 'Action Items' })}
-
- {`${actionItemsError.message}`}
-
+
+
+
+
+ Error occured while loading{' '}
+ {actionItemCategoriesError
+ ? 'Action Item Categories'
+ : membersError
+ ? 'Members List'
+ : 'Action Items List'}{' '}
+ Data
+
+ {actionItemCategoriesError
+ ? actionItemCategoriesError.message
+ : membersError
+ ? membersError.message
+ : actionItemsError?.message}
+
+
);
}
- 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 ? (
-
- ) : (
-
- )}
- {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)}
- />
-
- );
- },
- },
- ];
+ const actionItemCategories =
+ actionItemCategoriesData?.actionItemCategoriesByOrganization.filter(
+ (category) => !category.isDisabled,
+ );
+
+ const actionItemOnly = actionItemsData?.actionItemsByOrganization.filter(
+ (item) => item.event == null,
+ );
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="searchBy"
- />
-
-
-
-
-
-
-
- {tCommon('searchBy', { item: '' })}
-
-
- setSearchBy('assignee')}
- data-testid="assignee"
- >
- {t('assignee')}
-
- setSearchBy('category')}
- data-testid="category"
- >
- {t('category')}
-
-
-
-
-
+
+
+
+
+
-
- {tCommon('sort')}
-
-
- setSortBy('dueDate_DESC')}
- data-testid="dueDate_DESC"
+
- {t('latestDueDate')}
-
- setSortBy('dueDate_ASC')}
- data-testid="dueDate_ASC"
- >
- {t('earliestDueDate')}
-
-
-
-
-
+
+
+ {orderBy === 'Latest' ? t('latest') : t('earliest')}
+
+
+ handleSorting('Latest')}
+ data-testid="latest"
+ >
+ {t('latest')}
+
+ handleSorting('Earliest')}
+ data-testid="earliest"
+ >
+ {t('earliest')}
+
+
+
+
+
-
- {t('status')}
-
-
- setStatus(null)}
- data-testid="statusAll"
- >
- {tCommon('all')}
-
- setStatus(ItemStatus.Pending)}
- data-testid="statusPending"
+
- {tCommon('pending')}
-
- setStatus(ItemStatus.Completed)}
- data-testid="statusCompleted"
+
+ {actionItemCategoryName === ''
+ ? t('actionItemCategory')
+ : actionItemCategoryName}
+
+
+ {t('actionItemCategory')}
+
+
+
+ {actionItemCategories?.map((category, index) => (
+ {
+ setActionItemCategoryId(category._id);
+ setActionItemCategoryName(category.name);
+ }}
+ >
+ {category.name}
+
+ ))}
+
+
+
+
+
- {tCommon('completed')}
-
-
-
-
-
+
+ {actionItemStatus === '' ? t('status') : actionItemStatus}
+
+
{t('status')}
+
+
+ handleStatusFilter('Active')}
+ data-testid="activeActionItems"
+ >
+ {t('active')}
+
+ handleStatusFilter('Completed')}
+ data-testid="completedActionItems"
+ >
+ {t('completed')}
+
+
+
+
+
+
+ {!actionItemCategoryName && !actionItemStatus && (
+
+ {tCommon('noFiltersApplied')}
+
+ )}
+
+ {actionItemCategoryName !== '' && (
+
+ {actionItemCategoryName}
+ {
+ setActionItemCategoryName('');
+ setActionItemCategoryId('');
+ }}
+ data-testid="clearActionItemCategoryFilter"
+ />
+
+ )}
+
+ {actionItemStatus !== '' && (
+
+ {actionItemStatus}
+ setActionItemStatus('')}
+ data-testid="clearActionItemStatusFilter"
+ />
+
+ )}
+
+
+
-
- {/* 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}
- />
+
+
-
closeModal(ModalState.STATUS)}
- actionItemsRefetch={actionItemsRefetch}
+ {/* Create Modal */}
+
-
- {/* View Modal */}
- {actionItem && (
- closeModal(ModalState.VIEW)}
- item={actionItem}
- />
- )}
);
}
diff --git a/src/screens/OrganizationActionItems/OrganizationActionItemsErrorMocks.ts b/src/screens/OrganizationActionItems/OrganizationActionItemsErrorMocks.ts
new file mode 100644
index 0000000000..bedba6572b
--- /dev/null
+++ b/src/screens/OrganizationActionItems/OrganizationActionItemsErrorMocks.ts
@@ -0,0 +1,266 @@
+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/components/OrgSettings/AgendaItemCategories/AgendaCategoryCreateModal.test.tsx b/src/screens/OrganizationAgendaCategory/AgendaCategoryCreateModal.test.tsx
similarity index 100%
rename from src/components/OrgSettings/AgendaItemCategories/AgendaCategoryCreateModal.test.tsx
rename to src/screens/OrganizationAgendaCategory/AgendaCategoryCreateModal.test.tsx
diff --git a/src/components/OrgSettings/AgendaItemCategories/AgendaCategoryCreateModal.tsx b/src/screens/OrganizationAgendaCategory/AgendaCategoryCreateModal.tsx
similarity index 100%
rename from src/components/OrgSettings/AgendaItemCategories/AgendaCategoryCreateModal.tsx
rename to src/screens/OrganizationAgendaCategory/AgendaCategoryCreateModal.tsx
diff --git a/src/components/OrgSettings/AgendaItemCategories/AgendaCategoryDeleteModal.tsx b/src/screens/OrganizationAgendaCategory/AgendaCategoryDeleteModal.tsx
similarity index 100%
rename from src/components/OrgSettings/AgendaItemCategories/AgendaCategoryDeleteModal.tsx
rename to src/screens/OrganizationAgendaCategory/AgendaCategoryDeleteModal.tsx
diff --git a/src/components/OrgSettings/AgendaItemCategories/AgendaCategoryPreviewModal.tsx b/src/screens/OrganizationAgendaCategory/AgendaCategoryPreviewModal.tsx
similarity index 100%
rename from src/components/OrgSettings/AgendaItemCategories/AgendaCategoryPreviewModal.tsx
rename to src/screens/OrganizationAgendaCategory/AgendaCategoryPreviewModal.tsx
diff --git a/src/components/OrgSettings/AgendaItemCategories/AgendaCategoryUpdateModal.test.tsx b/src/screens/OrganizationAgendaCategory/AgendaCategoryUpdateModal.test.tsx
similarity index 100%
rename from src/components/OrgSettings/AgendaItemCategories/AgendaCategoryUpdateModal.test.tsx
rename to src/screens/OrganizationAgendaCategory/AgendaCategoryUpdateModal.test.tsx
diff --git a/src/components/OrgSettings/AgendaItemCategories/AgendaCategoryUpdateModal.tsx b/src/screens/OrganizationAgendaCategory/AgendaCategoryUpdateModal.tsx
similarity index 100%
rename from src/components/OrgSettings/AgendaItemCategories/AgendaCategoryUpdateModal.tsx
rename to src/screens/OrganizationAgendaCategory/AgendaCategoryUpdateModal.tsx
diff --git a/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.module.css b/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.module.css
similarity index 100%
rename from src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.module.css
rename to src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.module.css
diff --git a/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.test.tsx b/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.test.tsx
similarity index 96%
rename from src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.test.tsx
rename to src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.test.tsx
index e05edc665d..732db13a74 100644
--- a/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.test.tsx
+++ b/src/screens/OrganizationAgendaCategory/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/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.tsx b/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.tsx
similarity index 93%
rename from src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.tsx
rename to src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.tsx
index 884371d862..ed75456b9f 100644
--- a/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory.tsx
+++ b/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.tsx
@@ -1,7 +1,8 @@
import React, { useState } from 'react';
-import type { ChangeEvent, FC } from 'react';
+import type { ChangeEvent } 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';
@@ -16,10 +17,6 @@ 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.
*
@@ -27,14 +24,14 @@ interface InterfaceAgendaCategoryProps {
*
* @returns The rendered component.
*/
-
-const organizationAgendaCategory: FC
= ({
- orgId,
-}) => {
+function organizationAgendaCategory(): JSX.Element {
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);
@@ -59,7 +56,7 @@ const organizationAgendaCategory: FC = ({
error?: unknown | undefined;
refetch: () => void;
} = useQuery(AGENDA_ITEM_CATEGORY_LIST, {
- variables: { organizationId: orgId },
+ variables: { organizationId: currentUrl },
notifyOnNetworkStatusChange: true,
});
@@ -84,7 +81,7 @@ const organizationAgendaCategory: FC = ({
await createAgendaCategory({
variables: {
input: {
- organizationId: orgId,
+ organizationId: currentUrl,
name: formState.name,
description: formState.description,
},
@@ -135,7 +132,7 @@ const organizationAgendaCategory: FC = ({
}
return (
-
+
@@ -182,6 +179,6 @@ const organizationAgendaCategory: FC = ({
/>
);
-};
+}
export default organizationAgendaCategory;
diff --git a/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategoryErrorMocks.ts b/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategoryErrorMocks.ts
similarity index 100%
rename from src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategoryErrorMocks.ts
rename to src/screens/OrganizationAgendaCategory/OrganizationAgendaCategoryErrorMocks.ts
diff --git a/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategoryMocks.ts b/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategoryMocks.ts
similarity index 100%
rename from src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategoryMocks.ts
rename to src/screens/OrganizationAgendaCategory/OrganizationAgendaCategoryMocks.ts
diff --git a/src/screens/UserPortal/Campaigns/Campaigns.test.tsx b/src/screens/UserPortal/Campaigns/Campaigns.test.tsx
index 17b7eec4d5..443d643cff 100644
--- a/src/screens/UserPortal/Campaigns/Campaigns.test.tsx
+++ b/src/screens/UserPortal/Campaigns/Campaigns.test.tsx
@@ -155,17 +155,14 @@ 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(() => {
- expect(detailContainer).toBeInTheDocument();
- expect(detailContainer2).toBeInTheDocument();
+ const detailContainer = screen.getByTestId('detailContainer1');
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');
@@ -294,6 +291,18 @@ 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);
@@ -309,16 +318,4 @@ 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 f64401bca5..7b91fac025 100644
--- a/src/screens/UserPortal/Campaigns/CampaignsMocks.ts
+++ b/src/screens/UserPortal/Campaigns/CampaignsMocks.ts
@@ -1,63 +1,6 @@
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: {
@@ -230,7 +173,62 @@ export const MOCKS = [
},
},
},
- 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 EMPTY_MOCKS = [
@@ -251,7 +249,62 @@ export const EMPTY_MOCKS = [
},
},
},
- 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 USER_FUND_CAMPAIGNS_ERROR = [
@@ -268,5 +321,4 @@ export const USER_FUND_CAMPAIGNS_ERROR = [
},
error: new Error('Error fetching campaigns'),
},
- userDetailsQuery,
];
diff --git a/src/screens/UserPortal/Organizations/Organizations.test.tsx b/src/screens/UserPortal/Organizations/Organizations.test.tsx
index 163a755926..f8a6fc06a5 100644
--- a/src/screens/UserPortal/Organizations/Organizations.test.tsx
+++ b/src/screens/UserPortal/Organizations/Organizations.test.tsx
@@ -519,4 +519,44 @@ describe('Testing Organizations Screen [User Portal]', () => {
settingsBtn.click();
});
});
+ test('Rows per Page values', async () => {
+ render(
+
+
+
+
+
+
+
+
+ ,
+ );
+ await wait();
+ const dropdown = screen.getByTestId('table-pagination');
+ userEvent.click(dropdown);
+ expect(screen.queryByText('-1')).not.toBeInTheDocument();
+ expect(screen.getByText('5')).toBeInTheDocument();
+ expect(screen.getByText('10')).toBeInTheDocument();
+ expect(screen.getByText('30')).toBeInTheDocument();
+ expect(screen.getByText('All')).toBeInTheDocument();
+ });
+
+ test('Search input has correct placeholder text', async () => {
+ render(
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ await wait();
+
+ const searchInput = screen.getByPlaceholderText('Search Organization');
+ expect(searchInput).toBeInTheDocument();
+ });
});
diff --git a/src/screens/UserPortal/Organizations/Organizations.tsx b/src/screens/UserPortal/Organizations/Organizations.tsx
index d64babf4a1..59f5500d02 100644
--- a/src/screens/UserPortal/Organizations/Organizations.tsx
+++ b/src/screens/UserPortal/Organizations/Organizations.tsx
@@ -319,8 +319,8 @@ export default function organizations(): JSX.Element {
{
});
});
+ 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(() => {
@@ -338,18 +352,4 @@ 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 c7666987ff..9aa3780fbd 100644
--- a/src/screens/UserPortal/Pledges/PledgesMocks.ts
+++ b/src/screens/UserPortal/Pledges/PledgesMocks.ts
@@ -1,64 +1,11 @@
-import { DELETE_PLEDGE } from 'GraphQl/Mutations/PledgeMutation';
+import {
+ CREATE_PlEDGE,
+ DELETE_PLEDGE,
+ UPDATE_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: {
@@ -554,7 +501,62 @@ export const MOCKS = [
},
},
},
- 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 EMPTY_MOCKS = [
@@ -575,7 +577,6 @@ export const EMPTY_MOCKS = [
},
},
},
- userDetailsQuery,
];
export const USER_PLEDGES_ERROR = [
@@ -592,5 +593,4 @@ export const USER_PLEDGES_ERROR = [
},
error: new Error('Error fetching pledges'),
},
- userDetailsQuery,
];
diff --git a/src/screens/UserPortal/UserScreen/UserScreen.tsx b/src/screens/UserPortal/UserScreen/UserScreen.tsx
index 3bf84022cf..8543803933 100644
--- a/src/screens/UserPortal/UserScreen/UserScreen.tsx
+++ b/src/screens/UserPortal/UserScreen/UserScreen.tsx
@@ -1,7 +1,8 @@
import React, { useEffect, useState } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import { Navigate, Outlet, useLocation, useParams } from 'react-router-dom';
import { updateTargets } from 'state/action-creators';
+import { useAppDispatch } from 'state/hooks';
import type { RootState } from 'state/reducers';
import type { TargetsType } from 'state/reducers/routesReducer';
import styles from './UserScreen.module.css';
@@ -57,7 +58,7 @@ const UserScreen = (): JSX.Element => {
*/
// Initialize Redux dispatch
- const dispatch = useDispatch();
+ const dispatch = useAppDispatch();
/**
* Effect hook to update targets based on the organization ID.
diff --git a/src/state/hooks.ts b/src/state/hooks.ts
new file mode 100644
index 0000000000..161703cc49
--- /dev/null
+++ b/src/state/hooks.ts
@@ -0,0 +1,5 @@
+import { useDispatch } from 'react-redux';
+import type { AppDispatch } from './store';
+
+// Use throughout your app instead of plain `useDispatch`
+export const useAppDispatch = useDispatch.withTypes();
diff --git a/src/state/reducers/routesReducer.test.ts b/src/state/reducers/routesReducer.test.ts
index 8bdc1c069b..8d8de8a5dd 100644
--- a/src/state/reducers/routesReducer.test.ts
+++ b/src/state/reducers/routesReducer.test.ts
@@ -17,6 +17,7 @@ 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',
@@ -69,6 +70,11 @@ 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' },
{
@@ -120,6 +126,7 @@ 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' },
@@ -169,6 +176,11 @@ 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' },
{
@@ -216,6 +228,7 @@ 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',
@@ -271,6 +284,11 @@ 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 5d50f5402d..878fe73099 100644
--- a/src/state/reducers/routesReducer.ts
+++ b/src/state/reducers/routesReducer.ts
@@ -77,6 +77,11 @@ 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/state/store.ts b/src/state/store.ts
index 4ec777453a..fa4329c693 100644
--- a/src/state/store.ts
+++ b/src/state/store.ts
@@ -1,5 +1,8 @@
-import { applyMiddleware, createStore } from 'redux';
-import thunk from 'redux-thunk';
+import { configureStore } from '@reduxjs/toolkit';
import { reducers } from './reducers/index';
-export const store = createStore(reducers, {}, applyMiddleware(thunk));
+export const store = configureStore({
+ reducer: reducers,
+});
+
+export type AppDispatch = typeof store.dispatch;
diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts
index 495234a5a1..3bdf2a7d98 100644
--- a/src/utils/interfaces.ts
+++ b/src/utils/interfaces.ts
@@ -11,8 +11,6 @@ export interface InterfaceActionItemCategoryInfo {
_id: string;
name: string;
isDisabled: boolean;
- createdAt: string;
- creator: { _id: string; firstName: string; lastName: string };
}
export interface InterfaceActionItemCategoryList {
@@ -25,20 +23,18 @@ 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 | null;
+ postCompletionNotes: string;
assignmentDate: Date;
dueDate: Date;
completionDate: Date;
@@ -46,13 +42,12 @@ export interface InterfaceActionItemInfo {
event: {
_id: string;
title: string;
- } | null;
+ };
creator: {
_id: string;
firstName: string;
lastName: string;
};
- allotedHours: number | null;
}
export interface InterfaceActionItemList {
@@ -566,8 +561,3 @@ export interface InterfaceAgendaItemList {
export interface InterfaceMapType {
[key: string]: string;
}
-
-export interface InterfaceCustomFieldData {
- type: string;
- name: string;
-}
diff --git a/src/utils/useSession.test.tsx b/src/utils/useSession.test.tsx
new file mode 100644
index 0000000000..32287ccbb0
--- /dev/null
+++ b/src/utils/useSession.test.tsx
@@ -0,0 +1,544 @@
+import type { ReactNode } from 'react';
+import React from 'react';
+import { renderHook, act, waitFor } from '@testing-library/react';
+import { MockedProvider } from '@apollo/client/testing';
+import { toast } from 'react-toastify';
+import useSession from './useSession';
+import { GET_COMMUNITY_SESSION_TIMEOUT_DATA } from 'GraphQl/Queries/Queries';
+import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
+import { errorHandler } from 'utils/errorHandler';
+import { BrowserRouter } from 'react-router-dom';
+
+jest.mock('react-toastify', () => ({
+ toast: {
+ info: jest.fn(),
+ warning: jest.fn(),
+ error: jest.fn(),
+ },
+}));
+
+jest.mock('utils/errorHandler', () => ({
+ errorHandler: jest.fn(),
+}));
+
+jest.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+const MOCKS = [
+ {
+ request: {
+ query: GET_COMMUNITY_SESSION_TIMEOUT_DATA,
+ },
+ result: {
+ data: {
+ getCommunityData: {
+ timeout: 30,
+ },
+ },
+ },
+ delay: 100,
+ },
+ {
+ request: {
+ query: REVOKE_REFRESH_TOKEN,
+ },
+ result: {
+ data: {
+ revokeRefreshTokenForUser: true,
+ },
+ },
+ },
+];
+
+const wait = (ms: number): Promise =>
+ new Promise((resolve) => setTimeout(resolve, ms));
+
+describe('useSession Hook', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.spyOn(window, 'addEventListener').mockImplementation(jest.fn());
+ jest.spyOn(window, 'removeEventListener').mockImplementation(jest.fn());
+ Object.defineProperty(global, 'localStorage', {
+ value: {
+ clear: jest.fn(),
+ },
+ writable: true,
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ jest.useRealTimers();
+ jest.restoreAllMocks();
+ });
+
+ test('should handle visibility change to visible', async () => {
+ jest.useFakeTimers();
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'visible',
+ writable: true,
+ });
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ // Simulate visibility change
+ act(() => {
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ act(() => {
+ jest.advanceTimersByTime(15 * 60 * 1000);
+ });
+
+ await waitFor(() => {
+ expect(window.addEventListener).toHaveBeenCalledWith(
+ 'mousemove',
+ expect.any(Function),
+ );
+ expect(window.addEventListener).toHaveBeenCalledWith(
+ 'keydown',
+ expect.any(Function),
+ );
+ expect(toast.warning).toHaveBeenCalledWith('sessionWarning');
+ });
+
+ jest.useRealTimers();
+ });
+
+ test('should handle visibility change to hidden and ensure no warning appears in 15 minutes', async () => {
+ jest.useFakeTimers();
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'hidden',
+ writable: true,
+ });
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ act(() => {
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ act(() => {
+ jest.advanceTimersByTime(15 * 60 * 1000);
+ });
+
+ await waitFor(() => {
+ expect(window.removeEventListener).toHaveBeenCalledWith(
+ 'mousemove',
+ expect.any(Function),
+ );
+ expect(window.removeEventListener).toHaveBeenCalledWith(
+ 'keydown',
+ expect.any(Function),
+ );
+ expect(toast.warning).not.toHaveBeenCalled();
+ });
+
+ jest.useRealTimers();
+ });
+
+ test('should register event listeners on startSession', async () => {
+ const addEventListenerMock = jest.fn();
+ const originalWindowAddEventListener = window.addEventListener;
+ const originalDocumentAddEventListener = document.addEventListener;
+
+ window.addEventListener = addEventListenerMock;
+ document.addEventListener = addEventListenerMock;
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ expect(addEventListenerMock).toHaveBeenCalledWith(
+ 'mousemove',
+ expect.any(Function),
+ );
+ expect(addEventListenerMock).toHaveBeenCalledWith(
+ 'keydown',
+ expect.any(Function),
+ );
+ expect(addEventListenerMock).toHaveBeenCalledWith(
+ 'visibilitychange',
+ expect.any(Function),
+ );
+
+ window.addEventListener = originalWindowAddEventListener;
+ document.addEventListener = originalDocumentAddEventListener;
+ });
+
+ test('should call handleLogout after session timeout', async () => {
+ jest.useFakeTimers();
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ act(() => {
+ jest.advanceTimersByTime(31 * 60 * 1000);
+ });
+
+ await waitFor(() => {
+ expect(global.localStorage.clear).toHaveBeenCalled();
+ expect(toast.warning).toHaveBeenCalledTimes(2);
+ expect(toast.warning).toHaveBeenNthCalledWith(1, 'sessionWarning');
+ expect(toast.warning).toHaveBeenNthCalledWith(2, 'sessionLogout', {
+ autoClose: false,
+ });
+ });
+ });
+
+ test('should show a warning toast before session expiration', async () => {
+ jest.useFakeTimers();
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ act(() => {
+ jest.advanceTimersByTime(15 * 60 * 1000);
+ });
+
+ await waitFor(() =>
+ expect(toast.warning).toHaveBeenCalledWith('sessionWarning'),
+ );
+
+ jest.useRealTimers();
+ });
+
+ test('should handle error when revoking token fails', async () => {
+ const consoleErrorMock = jest.spyOn(console, 'error').mockImplementation();
+
+ const errorMocks = [
+ {
+ request: {
+ query: GET_COMMUNITY_SESSION_TIMEOUT_DATA,
+ },
+ result: {
+ data: {
+ getCommunityData: {
+ timeout: 30,
+ },
+ },
+ },
+ delay: 1000,
+ },
+ {
+ request: {
+ query: REVOKE_REFRESH_TOKEN,
+ },
+ error: new Error('Failed to revoke refresh token'),
+ },
+ ];
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ act(() => {
+ result.current.startSession();
+ result.current.handleLogout();
+ });
+
+ await waitFor(() =>
+ expect(consoleErrorMock).toHaveBeenCalledWith(
+ 'Error revoking refresh token:',
+ expect.any(Error),
+ ),
+ );
+
+ consoleErrorMock.mockRestore();
+ });
+
+ test('should set session timeout based on fetched data', async () => {
+ jest.spyOn(global, 'setTimeout');
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ expect(global.setTimeout).toHaveBeenCalled();
+ });
+
+ test('should call errorHandler on query error', async () => {
+ const errorMocks = [
+ {
+ request: {
+ query: GET_COMMUNITY_SESSION_TIMEOUT_DATA,
+ },
+ error: new Error('An error occurred'),
+ },
+ ];
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ await waitFor(() => expect(errorHandler).toHaveBeenCalled());
+ });
+ //dfghjkjhgfds
+
+ test('should remove event listeners on endSession', async () => {
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ // Mock the removeEventListener functions for both window and document
+ const removeEventListenerMock = jest.fn();
+
+ // Temporarily replace the real methods with the mock
+ const originalWindowRemoveEventListener = window.removeEventListener;
+ const originalDocumentRemoveEventListener = document.removeEventListener;
+
+ window.removeEventListener = removeEventListenerMock;
+ document.removeEventListener = removeEventListenerMock;
+
+ // await waitForNextUpdate();
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ act(() => {
+ result.current.endSession();
+ });
+
+ // Test that event listeners were removed
+ expect(removeEventListenerMock).toHaveBeenCalledWith(
+ 'mousemove',
+ expect.any(Function),
+ );
+ expect(removeEventListenerMock).toHaveBeenCalledWith(
+ 'keydown',
+ expect.any(Function),
+ );
+ expect(removeEventListenerMock).toHaveBeenCalledWith(
+ 'visibilitychange',
+ expect.any(Function),
+ );
+
+ // Restore the original removeEventListener functions
+ window.removeEventListener = originalWindowRemoveEventListener;
+ document.removeEventListener = originalDocumentRemoveEventListener;
+ });
+
+ test('should call initialize timers when session is still active when the user returns to the tab', async () => {
+ jest.useFakeTimers();
+ jest.spyOn(global, 'setTimeout').mockImplementation(jest.fn());
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ });
+
+ jest.advanceTimersByTime(1000);
+
+ // Set initial visibility state to visible
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'visible',
+ writable: true,
+ });
+
+ // Start the session
+ act(() => {
+ result.current.startSession();
+ jest.advanceTimersByTime(10 * 60 * 1000); // Fast-forward
+ });
+
+ // Simulate the user leaving the tab (set visibility to hidden)
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'hidden',
+ writable: true,
+ });
+
+ act(() => {
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ // Fast-forward time by more than the session timeout
+ act(() => {
+ jest.advanceTimersByTime(5 * 60 * 1000); // Fast-forward
+ });
+
+ // Simulate the user returning to the tab
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'visible',
+ writable: true,
+ });
+
+ act(() => {
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ jest.advanceTimersByTime(1000);
+
+ expect(global.setTimeout).toHaveBeenCalled();
+
+ // Restore real timers
+ jest.useRealTimers();
+ });
+
+ test('should call handleLogout when session expires due to inactivity away from tab', async () => {
+ jest.useFakeTimers(); // Use fake timers to control time
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ });
+
+ jest.advanceTimersByTime(1000);
+
+ // Set initial visibility state to visible
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'visible',
+ writable: true,
+ });
+
+ // Start the session
+ act(() => {
+ result.current.startSession();
+ jest.advanceTimersByTime(10 * 60 * 1000); // Fast-forward
+ });
+
+ // Simulate the user leaving the tab (set visibility to hidden)
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'hidden',
+ writable: true,
+ });
+
+ act(() => {
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ // Fast-forward time by more than the session timeout
+ act(() => {
+ jest.advanceTimersByTime(32 * 60 * 1000); // Fast-forward by 32 minutes
+ });
+
+ // Simulate the user returning to the tab
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'visible',
+ writable: true,
+ });
+
+ act(() => {
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ jest.advanceTimersByTime(250);
+
+ await waitFor(() => {
+ expect(global.localStorage.clear).toHaveBeenCalled();
+ expect(toast.warning).toHaveBeenCalledWith('sessionLogout', {
+ autoClose: false,
+ });
+ });
+
+ // Restore real timers
+ jest.useRealTimers();
+ });
+
+ test('should handle logout and revoke token', async () => {
+ jest.useFakeTimers();
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ act(() => {
+ result.current.startSession();
+ result.current.handleLogout();
+ });
+
+ await waitFor(() => {
+ expect(global.localStorage.clear).toHaveBeenCalled();
+ expect(toast.warning).toHaveBeenCalledWith('sessionLogout', {
+ autoClose: false,
+ });
+ });
+
+ jest.useRealTimers();
+ });
+});
diff --git a/src/utils/useSession.tsx b/src/utils/useSession.tsx
new file mode 100644
index 0000000000..4279e7c850
--- /dev/null
+++ b/src/utils/useSession.tsx
@@ -0,0 +1,164 @@
+import { useMutation, useQuery } from '@apollo/client';
+import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
+import { GET_COMMUNITY_SESSION_TIMEOUT_DATA } from 'GraphQl/Queries/Queries';
+import { t } from 'i18next';
+import { useEffect, useState, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
+import { toast } from 'react-toastify';
+import { errorHandler } from 'utils/errorHandler';
+
+type UseSessionReturnType = {
+ startSession: () => void;
+ endSession: () => void;
+ handleLogout: () => void;
+ extendSession: () => void; //for when logged in already, simply extend session
+};
+
+/**
+ * Custom hook for managing user session timeouts in a React application.
+ *
+ * This hook handles:
+ * - Starting and ending the user session.
+ * - Displaying a warning toast at half of the session timeout duration.
+ * - Logging the user out and displaying a session expiration toast when the session times out.
+ * - Automatically resetting the timers when user activity is detected.
+ * - Pausing session timers when the tab is inactive and resuming them when it becomes active again.
+ *
+ * @returns UseSessionReturnType - An object with methods to start and end the session, and to handle logout.
+ */
+const useSession = (): UseSessionReturnType => {
+ const { t: tCommon } = useTranslation('common');
+
+ let startTime: number;
+ let timeoutDuration: number;
+ const [sessionTimeout, setSessionTimeout] = useState(30);
+ // const sessionTimeout = 30;
+ const sessionTimerRef = useRef(null);
+ const warningTimerRef = useRef(null);
+ const navigate = useNavigate();
+
+ const [revokeRefreshToken] = useMutation(REVOKE_REFRESH_TOKEN);
+ const { data, error: queryError } = useQuery(
+ GET_COMMUNITY_SESSION_TIMEOUT_DATA,
+ );
+
+ useEffect(() => {
+ if (queryError) {
+ errorHandler(t, queryError as Error);
+ } else {
+ const sessionTimeoutData = data?.getCommunityData;
+ if (sessionTimeoutData) {
+ setSessionTimeout(sessionTimeoutData.timeout);
+ }
+ }
+ }, [data, queryError]);
+
+ const resetTimers = (): void => {
+ if (sessionTimerRef.current) clearTimeout(sessionTimerRef.current);
+ if (warningTimerRef.current) clearTimeout(warningTimerRef.current);
+ };
+
+ const endSession = (): void => {
+ resetTimers();
+ window.removeEventListener('mousemove', extendSession);
+ window.removeEventListener('keydown', extendSession);
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
+ };
+
+ const handleLogout = async (): Promise => {
+ try {
+ await revokeRefreshToken();
+ } catch (error) {
+ console.error('Error revoking refresh token:', error);
+ // toast.error('Failed to revoke session. Please try again.');
+ }
+ localStorage.clear();
+ endSession();
+ navigate('/');
+ toast.warning(tCommon('sessionLogout'), { autoClose: false });
+ };
+
+ const initializeTimers = (
+ timeLeft?: number,
+ warningTimeLeft?: number,
+ ): void => {
+ const warningTime = warningTimeLeft ?? sessionTimeout / 2;
+ const sessionTimeoutInMilliseconds =
+ (timeLeft || sessionTimeout) * 60 * 1000;
+ const warningTimeInMilliseconds = warningTime * 60 * 1000;
+
+ timeoutDuration = sessionTimeoutInMilliseconds;
+ startTime = Date.now();
+
+ warningTimerRef.current = setTimeout(() => {
+ toast.warning(tCommon('sessionWarning'));
+ }, warningTimeInMilliseconds);
+
+ sessionTimerRef.current = setTimeout(async () => {
+ await handleLogout();
+ }, sessionTimeoutInMilliseconds);
+ };
+
+ const extendSession = (): void => {
+ resetTimers();
+ initializeTimers();
+ };
+
+ const startSession = (): void => {
+ resetTimers();
+ initializeTimers();
+ window.removeEventListener('mousemove', extendSession);
+ window.removeEventListener('keydown', extendSession);
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
+ window.addEventListener('mousemove', extendSession);
+ window.addEventListener('keydown', extendSession);
+ document.addEventListener('visibilitychange', handleVisibilityChange);
+ };
+
+ const handleVisibilityChange = async (): Promise => {
+ if (document.visibilityState === 'hidden') {
+ window.removeEventListener('mousemove', extendSession);
+ window.removeEventListener('keydown', extendSession);
+ resetTimers(); // Optionally reset timers to prevent them from running in the background
+ } else if (document.visibilityState === 'visible') {
+ window.removeEventListener('mousemove', extendSession);
+ window.removeEventListener('keydown', extendSession); // Ensure no duplicates
+ window.addEventListener('mousemove', extendSession);
+ window.addEventListener('keydown', extendSession);
+
+ // Calculate remaining time now that the tab is active again
+ const elapsedTime = Date.now() - startTime;
+ const remainingTime = timeoutDuration - elapsedTime;
+
+ const remainingSessionTime = Math.max(remainingTime, 0); // Ensures the remaining time is non-negative and measured in ms;
+
+ if (remainingSessionTime > 0) {
+ // Calculate remaining warning time only if session time is positive
+ const remainingWarningTime = Math.max(remainingSessionTime / 2, 0);
+ initializeTimers(
+ remainingSessionTime / 60 / 1000,
+ remainingWarningTime / 60 / 1000,
+ );
+ } else {
+ // Handle session expiration immediately if time has run out
+ await handleLogout();
+ }
+ }
+ };
+
+ useEffect(() => {
+ return () => {
+ endSession();
+ };
+ }, []);
+
+ return {
+ startSession,
+ endSession,
+ handleLogout,
+ extendSession,
+ };
+};
+
+export default useSession;