From 01d010193e6801cda5b3de8577f2357b61e6d9cc Mon Sep 17 00:00:00 2001 From: Shiva <148421597+shivasankaran18@users.noreply.github.com> Date: Wed, 8 Jan 2025 01:01:08 +0530 Subject: [PATCH 01/32] Refactor: Vitest to src/screens/OrganizationFundCampaign (#3193) * refactor:vitest to screens/OrgFundCamp * fix:coderabit sugg --- ...nModal.test.tsx => CampaignModal.spec.tsx} | 26 ++++---- ....tsx => OrganizationFundCampaign.spec.tsx} | 59 +++++++++++-------- 2 files changed, 50 insertions(+), 35 deletions(-) rename src/screens/OrganizationFundCampaign/{CampaignModal.test.tsx => CampaignModal.spec.tsx} (95%) rename src/screens/OrganizationFundCampaign/{OrganizationFundCampaign.test.tsx => OrganizationFundCampaign.spec.tsx} (88%) diff --git a/src/screens/OrganizationFundCampaign/CampaignModal.test.tsx b/src/screens/OrganizationFundCampaign/CampaignModal.spec.tsx similarity index 95% rename from src/screens/OrganizationFundCampaign/CampaignModal.test.tsx rename to src/screens/OrganizationFundCampaign/CampaignModal.spec.tsx index 2a5a61a22b..f20b1ace3d 100644 --- a/src/screens/OrganizationFundCampaign/CampaignModal.test.tsx +++ b/src/screens/OrganizationFundCampaign/CampaignModal.spec.tsx @@ -21,19 +21,21 @@ import { toast } from 'react-toastify'; import { MOCKS, MOCK_ERROR } from './OrganizationFundCampaignMocks'; import type { InterfaceCampaignModal } from './CampaignModal'; import CampaignModal from './CampaignModal'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); -jest.mock('@mui/x-date-pickers/DateTimePicker', () => { +vi.mock('@mui/x-date-pickers/DateTimePicker', async () => { + const actual = await vi.importActual( + '@mui/x-date-pickers/DesktopDateTimePicker', + ); return { - DateTimePicker: jest.requireActual( - '@mui/x-date-pickers/DesktopDateTimePicker', - ).DesktopDateTimePicker, + DateTimePicker: actual.DesktopDateTimePicker, }; }); @@ -46,7 +48,7 @@ const translations = JSON.parse( const campaignProps: InterfaceCampaignModal[] = [ { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), fundId: 'fundId', orgId: 'orgId', campaign: { @@ -58,12 +60,12 @@ const campaignProps: InterfaceCampaignModal[] = [ currency: 'USD', createdAt: '2021-01-01', }, - refetchCampaign: jest.fn(), + refetchCampaign: vi.fn(), mode: 'create', }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), fundId: 'fundId', orgId: 'orgId', campaign: { @@ -75,7 +77,7 @@ const campaignProps: InterfaceCampaignModal[] = [ currency: 'USD', createdAt: '2021-01-01', }, - refetchCampaign: jest.fn(), + refetchCampaign: vi.fn(), mode: 'edit', }, ]; @@ -100,7 +102,7 @@ const renderCampaignModal = ( describe('CampaignModal', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); cleanup(); }); diff --git a/src/screens/OrganizationFundCampaign/OrganizationFundCampaign.test.tsx b/src/screens/OrganizationFundCampaign/OrganizationFundCampaign.spec.tsx similarity index 88% rename from src/screens/OrganizationFundCampaign/OrganizationFundCampaign.test.tsx rename to src/screens/OrganizationFundCampaign/OrganizationFundCampaign.spec.tsx index 9c169e355a..68bbc325b0 100644 --- a/src/screens/OrganizationFundCampaign/OrganizationFundCampaign.test.tsx +++ b/src/screens/OrganizationFundCampaign/OrganizationFundCampaign.spec.tsx @@ -3,17 +3,11 @@ 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 { - cleanup, - fireEvent, - render, - screen, - waitFor, -} 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 { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { MemoryRouter, Route, Routes, useParams } from 'react-router-dom'; import { store } from '../../state/store'; import { StaticMockLink } from '../../utils/StaticMockLink'; import i18nForTest from '../../utils/i18nForTest'; @@ -24,19 +18,21 @@ import { MOCK_ERROR, } from './OrganizationFundCampaignMocks'; import type { ApolloLink } from '@apollo/client'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); -jest.mock('@mui/x-date-pickers/DateTimePicker', () => { +vi.mock('@mui/x-date-pickers/DateTimePicker', async () => { + const actual = await vi.importActual( + '@mui/x-date-pickers/DesktopDateTimePicker', + ); return { - DateTimePicker: jest.requireActual( - '@mui/x-date-pickers/DesktopDateTimePicker', - ).DesktopDateTimePicker, + DateTimePicker: actual.DesktopDateTimePicker, }; }); @@ -83,21 +79,25 @@ const renderFundCampaign = (link: ApolloLink): RenderResult => { describe('FundCampaigns Screen', () => { beforeEach(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: 'orgId', fundId: 'fundId' }), - })); + vi.mock('react-router-dom', async () => { + const actualDom = await vi.importActual('react-router-dom'); + return { + ...actualDom, + useParams: vi.fn(), + }; + }); }); afterEach(() => { - cleanup(); + vi.clearAllMocks(); }); - afterAll(() => { - jest.clearAllMocks(); - }); + const mockRouteParams = (orgId = 'orgId', fundId = 'fundId'): void => { + vi.mocked(useParams).mockReturnValue({ orgId, fundId }); + }; it('should render the Campaign Pledge screen', async () => { + mockRouteParams(); renderFundCampaign(link1); await waitFor(() => { expect(screen.getByTestId('searchFullName')).toBeInTheDocument(); @@ -108,6 +108,7 @@ describe('FundCampaigns Screen', () => { }); it('should redirect to fallback URL if URL params are undefined', async () => { + mockRouteParams('', ''); render( @@ -136,6 +137,7 @@ describe('FundCampaigns Screen', () => { }); it('open and close Create Campaign modal', async () => { + mockRouteParams(); renderFundCampaign(link1); const addCampaignBtn = await screen.findByTestId('addCampaignBtn'); @@ -152,6 +154,7 @@ describe('FundCampaigns Screen', () => { }); it('open and close update campaign modal', async () => { + mockRouteParams(); renderFundCampaign(link1); await waitFor(() => { @@ -174,6 +177,7 @@ describe('FundCampaigns Screen', () => { }); it('Search the Campaigns list by Name', async () => { + mockRouteParams(); renderFundCampaign(link1); const searchField = await screen.findByTestId('searchFullName'); fireEvent.change(searchField, { @@ -187,6 +191,7 @@ describe('FundCampaigns Screen', () => { }); it('should render the Campaign screen with error', async () => { + mockRouteParams(); renderFundCampaign(link2); await waitFor(() => { expect(screen.getByTestId('errorMsg')).toBeInTheDocument(); @@ -194,6 +199,7 @@ describe('FundCampaigns Screen', () => { }); it('renders the empty campaign component', async () => { + mockRouteParams(); renderFundCampaign(link3); await waitFor(() => expect( @@ -203,6 +209,7 @@ describe('FundCampaigns Screen', () => { }); it('Sort the Campaigns list by Latest end Date', async () => { + mockRouteParams(); renderFundCampaign(link1); const sortBtn = await screen.findByTestId('filter'); @@ -224,6 +231,7 @@ describe('FundCampaigns Screen', () => { }); it('Sort the Campaigns list by Earliest end Date', async () => { + mockRouteParams(); renderFundCampaign(link1); const sortBtn = await screen.findByTestId('filter'); @@ -245,6 +253,7 @@ describe('FundCampaigns Screen', () => { }); it('Sort the Campaigns list by lowest goal', async () => { + mockRouteParams(); renderFundCampaign(link1); const sortBtn = await screen.findByTestId('filter'); @@ -264,6 +273,7 @@ describe('FundCampaigns Screen', () => { }); it('Sort the Campaigns list by highest goal', async () => { + mockRouteParams(); renderFundCampaign(link1); const sortBtn = await screen.findByTestId('filter'); @@ -283,6 +293,7 @@ describe('FundCampaigns Screen', () => { }); it('Click on Campaign Name', async () => { + mockRouteParams(); renderFundCampaign(link1); const campaignName = await screen.findAllByTestId('campaignName'); @@ -295,6 +306,7 @@ describe('FundCampaigns Screen', () => { }); it('Click on View Pledge', async () => { + mockRouteParams(); renderFundCampaign(link1); const viewBtn = await screen.findAllByTestId('viewBtn'); @@ -307,6 +319,7 @@ describe('FundCampaigns Screen', () => { }); it('should render the Fund screen on fund breadcrumb click', async () => { + mockRouteParams(); renderFundCampaign(link1); const fundBreadcrumb = await screen.findByTestId('fundsLink'); From 3b168aedf7861dd5f0f0d578c05f31a794f0826e Mon Sep 17 00:00:00 2001 From: Peter Harrison <16875803+palisadoes@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:52:01 -0800 Subject: [PATCH 02/32] Update jest.config.js --- jest.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jest.config.js b/jest.config.js index 75e0cc5b4d..dffec5db18 100644 --- a/jest.config.js +++ b/jest.config.js @@ -72,8 +72,8 @@ export default { ], coverageThreshold: { global: { - lines: 20, - statements: 20, + lines: 1, + statements: 1, }, }, testPathIgnorePatterns: [ From ef5a206c646c343de620942047d737708e0e5b99 Mon Sep 17 00:00:00 2001 From: Mehul Aggarwal <88583647+AceHunterr@users.noreply.github.com> Date: Wed, 8 Jan 2025 01:35:50 +0530 Subject: [PATCH 03/32] Fixes #2986 - Multiple UI Updates (#3165) * UI fixes on organisation pages * Added TSDoc for Truncated Text * Added Debouncer * Update src/components/OrgListCard/OrgListCard.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Added code rabbit suggestions * Fixed test error --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/assets/css/app.css | 21 +++++ .../EventCalendar/EventCalendar.module.css | 0 src/components/EventCalendar/EventHeader.tsx | 2 + src/components/OrgListCard/OrgListCard.tsx | 21 ++--- src/components/OrgListCard/TruncatedText.tsx | 80 +++++++++++++++++++ src/components/OrgListCard/useDebounce.tsx | 42 ++++++++++ .../UsersTableItem/UsersTableItem.tsx | 1 + src/screens/OrgList/OrgList.tsx | 3 + .../OrganizationPeople/OrganizationPeople.tsx | 56 ++++++++++--- src/style/app.module.css | 62 ++++++++++++-- 10 files changed, 263 insertions(+), 25 deletions(-) create mode 100644 src/components/EventCalendar/EventCalendar.module.css create mode 100644 src/components/OrgListCard/TruncatedText.tsx create mode 100644 src/components/OrgListCard/useDebounce.tsx diff --git a/src/assets/css/app.css b/src/assets/css/app.css index b3a8613975..bd34d56907 100644 --- a/src/assets/css/app.css +++ b/src/assets/css/app.css @@ -3442,6 +3442,7 @@ textarea.form-control.is-invalid { } } +/* To remove the green and replace by greyish hover , make changes here */ .btn:hover { color: var(--bs-btn-hover-color); background-color: var(--bs-btn-hover-bg); @@ -14066,6 +14067,7 @@ fieldset:disabled .btn { .btn-warning, .btn-info { color: #fff; + /* isolation: isolate; */ } .btn-primary:hover, @@ -14079,8 +14081,27 @@ fieldset:disabled .btn { .btn-info:hover, .btn-info:active { color: #fff !important; + box-shadow: inset 50px 50px 40px rgba(0, 0, 0, 0.5); + background-blend-mode: multiply; + /* background-color: #6c757d ; */ + /* filter: brightness(0.85); */ } +/* .btn-primary{ + --hover-bg: #6c757d !important; +} + + +.btn-primary:hover, +.btn-primary:active{ + --hover-bg: hsl(var(--button-hue, 0), 100%, 60%) !important; +} + +.btn-primary:hover, +.btn-primary:active{ + --hover-bg: hsl(var(--button-hue, 0), 100%, 0%) !important; +} */ + .btn-outline-primary:hover, .btn-outline-primary:active, .btn-outline-secondary:hover, diff --git a/src/components/EventCalendar/EventCalendar.module.css b/src/components/EventCalendar/EventCalendar.module.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/EventCalendar/EventHeader.tsx b/src/components/EventCalendar/EventHeader.tsx index d8f949ca97..d338de3b82 100644 --- a/src/components/EventCalendar/EventHeader.tsx +++ b/src/components/EventCalendar/EventHeader.tsx @@ -72,6 +72,7 @@ function eventHeader({ id="dropdown-basic" className={styles.dropdown} data-testid="selectViewType" + style={{ width: '100%' }} > {viewType} @@ -100,6 +101,7 @@ function eventHeader({ id="dropdown-basic" className={styles.dropdown} data-testid="eventType" + style={{ width: '100%' }} > {t('eventType')} diff --git a/src/components/OrgListCard/OrgListCard.tsx b/src/components/OrgListCard/OrgListCard.tsx index 10365d2364..cf651e9dfe 100644 --- a/src/components/OrgListCard/OrgListCard.tsx +++ b/src/components/OrgListCard/OrgListCard.tsx @@ -1,4 +1,6 @@ import React from 'react'; +import TruncatedText from './TruncatedText'; +// import {useState} from 'react'; import FlaskIcon from 'assets/svgs/flask.svg?react'; import Button from 'react-bootstrap/Button'; import { useTranslation } from 'react-i18next'; @@ -94,17 +96,18 @@ function orgListCard(props: InterfaceOrgListCardProps): JSX.Element {

{name}

{/* Description of the organization */} -
- {userData?.organizations[0].description} -
+
+ +
+ {/* Display the organization address if available */} - {address && address.city && ( + {address?.city && (
-
- {address.line1}, - {address.city}, - {address.countryCode} -
+
)} {/* Display the number of admins and members */} diff --git a/src/components/OrgListCard/TruncatedText.tsx b/src/components/OrgListCard/TruncatedText.tsx new file mode 100644 index 0000000000..94617178cb --- /dev/null +++ b/src/components/OrgListCard/TruncatedText.tsx @@ -0,0 +1,80 @@ +import React, { useState, useEffect, useRef } from 'react'; +import useDebounce from './useDebounce'; + +/** + * Props for the `TruncatedText` component. + * + * Includes the text to be displayed and an optional maximum width override. + */ +interface InterfaceTruncatedTextProps { + /** The full text to display. It may be truncated if it exceeds the maximum width. */ + text: string; + /** Optional: Override the maximum width for truncation. */ + maxWidthOverride?: number; +} + +/** + * A React functional component that displays text and truncates it with an ellipsis (`...`) + * if the text exceeds the available width or the `maxWidthOverride` value. + * + * The component adjusts the truncation dynamically based on the available space + * or the `maxWidthOverride` value. It also listens for window resize events to reapply truncation. + * + * @param props - The props for the component. + * @returns A heading element (`
`) containing the truncated or full text. + * + * @example + * ```tsx + * + * ``` + */ +const TruncatedText: React.FC = ({ + text, + maxWidthOverride, +}) => { + const [truncatedText, setTruncatedText] = useState(''); + const textRef = useRef(null); + + const { debouncedCallback, cancel } = useDebounce(() => { + truncateText(); + }, 100); + + /** + * Truncate the text based on the available width or the `maxWidthOverride` value. + */ + const truncateText = (): void => { + const element = textRef.current; + if (element) { + const maxWidth = maxWidthOverride || element.offsetWidth; + const fullText = text; + + const computedStyle = getComputedStyle(element); + const fontSize = parseFloat(computedStyle.fontSize); + const charPerPx = 0.065 + fontSize * 0.002; + const maxChars = Math.floor(maxWidth * charPerPx); + + setTruncatedText( + fullText.length > maxChars + ? `${fullText.slice(0, maxChars - 3)}...` + : fullText, + ); + } + }; + + useEffect(() => { + truncateText(); + window.addEventListener('resize', debouncedCallback); + return () => { + cancel(); + window.removeEventListener('resize', debouncedCallback); + }; + }, [text, maxWidthOverride, debouncedCallback, cancel]); + + return ( +
+ {truncatedText} +
+ ); +}; + +export default TruncatedText; diff --git a/src/components/OrgListCard/useDebounce.tsx b/src/components/OrgListCard/useDebounce.tsx new file mode 100644 index 0000000000..8ad30386e0 --- /dev/null +++ b/src/components/OrgListCard/useDebounce.tsx @@ -0,0 +1,42 @@ +import { useRef, useCallback } from 'react'; + +/** + * A custom React hook for debouncing a callback function. + * It delays the execution of the callback until after a specified delay has elapsed + * since the last time the debounced function was invoked. + * + * @param callback - The function to debounce. + * @param delay - The delay in milliseconds to wait before invoking the callback. + * @returns An object with the `debouncedCallback` function and a `cancel` method to clear the timeout. + */ +function useDebounce void>( + callback: T, + delay: number, +): { debouncedCallback: (...args: Parameters) => void; cancel: () => void } { + const timeoutRef = useRef(); + + /** + * The debounced version of the provided callback function. + * This function resets the debounce timer on each call, ensuring the callback + * is invoked only after the specified delay has elapsed without further calls. + * + * @param args - The arguments to pass to the callback when invoked. + */ + const debouncedCallback = useCallback( + (...args: Parameters) => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = window.setTimeout(() => { + callback(...args); + }, delay); + }, + [callback, delay], + ); + + const cancel = useCallback(() => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }, []); + + return { debouncedCallback, cancel }; +} + +export default useDebounce; diff --git a/src/components/UsersTableItem/UsersTableItem.tsx b/src/components/UsersTableItem/UsersTableItem.tsx index 9e94b8a9f5..6da3c1d6f4 100644 --- a/src/components/UsersTableItem/UsersTableItem.tsx +++ b/src/components/UsersTableItem/UsersTableItem.tsx @@ -161,6 +161,7 @@ const UsersTableItem = (props: Props): JSX.Element => { {user.user.email} - -
-
+
+ +
+
- - - Sort - Filter: {filteringBy} - - } - onSelect={(eventKey) => setFilteringBy(eventKey as FilterPeriod)} - > - This Month - This Year - All - - - Sort - Sort - - } - onSelect={ - /*istanbul ignore next*/ - (eventKey) => setSortOrder(eventKey as 'ascending' | 'descending') + setFilteringBy(value as FilterPeriod)} + dataTestIdPrefix="filter-dropdown" + className={`${styles.dropdown} mx-4`} + buttonLabel="Filter" + /> + + setSortOrder(value as 'ascending' | 'descending') } - > - Ascending - Descending - + dataTestIdPrefix="sort-dropdown" + buttonLabel="Sort" + />
- {/*

{totalMembers}

*/} diff --git a/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.spec.tsx b/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.spec.tsx index 27eec94851..90c2a105ce 100644 --- a/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.spec.tsx +++ b/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.spec.tsx @@ -125,9 +125,9 @@ describe('Testing Organisation Action Item Categories', () => { // Filter by All fireEvent.click(filterBtn); await waitFor(() => { - expect(screen.getByTestId('statusAll')).toBeInTheDocument(); + expect(screen.getByTestId('all')).toBeInTheDocument(); }); - fireEvent.click(screen.getByTestId('statusAll')); + fireEvent.click(screen.getByTestId('all')); await waitFor(() => { expect(screen.getByText('Category 1')).toBeInTheDocument(); @@ -137,9 +137,9 @@ describe('Testing Organisation Action Item Categories', () => { // Filter by Disabled fireEvent.click(filterBtn); await waitFor(() => { - expect(screen.getByTestId('statusDisabled')).toBeInTheDocument(); + expect(screen.getByTestId('disabled')).toBeInTheDocument(); }); - fireEvent.click(screen.getByTestId('statusDisabled')); + fireEvent.click(screen.getByTestId('disabled')); await waitFor(() => { expect(screen.queryByText('Category 1')).toBeNull(); expect(screen.getByText('Category 2')).toBeInTheDocument(); @@ -154,9 +154,9 @@ describe('Testing Organisation Action Item Categories', () => { fireEvent.click(filterBtn); await waitFor(() => { - expect(screen.getByTestId('statusActive')).toBeInTheDocument(); + expect(screen.getByTestId('active')).toBeInTheDocument(); }); - fireEvent.click(screen.getByTestId('statusActive')); + fireEvent.click(screen.getByTestId('active')); await waitFor(() => { expect(screen.getByText('Category 1')).toBeInTheDocument(); expect(screen.queryByText('Category 2')).toBeNull(); diff --git a/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.tsx b/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.tsx index 3f1001c88b..a1f31e6da1 100644 --- a/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.tsx +++ b/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react'; import React, { useCallback, useEffect, useState } from 'react'; -import { Button, Dropdown, Form } from 'react-bootstrap'; +import { Button, Form } from 'react-bootstrap'; import styles from '../../../style/app.module.css'; import { useTranslation } from 'react-i18next'; @@ -8,13 +8,7 @@ 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 { Circle, Search, WarningAmberRounded } from '@mui/icons-material'; import { DataGrid, type GridCellParams, @@ -23,6 +17,7 @@ import { import dayjs from 'dayjs'; import { Chip, Stack } from '@mui/material'; import CategoryModal from './CategoryModal'; +import SortingButton from 'subComponents/SortingButton'; enum ModalState { SAME = 'same', @@ -311,63 +306,47 @@ const OrgActionItemCategories: FC = ({
- - - - {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')} - - - + + setSortBy(value as 'createdAt_DESC' | 'createdAt_ASC') + } + dataTestIdPrefix="sort" + buttonLabel={tCommon('sort')} + className={styles.dropdown} + /> + + setStatus(value === 'all' ? null : (value as CategoryStatus)) + } + dataTestIdPrefix="filter" + buttonLabel={t('status')} + className={styles.dropdown} + />
+
- {/* Dropdown for filtering members */} - - {/* Dropdown for sorting by name */} - + : t('searchByLastName') + } + onSortChange={(value) => + setSearchByFirstName(value === 'searchByFirstName') + } + dataTestIdPrefix="nameFilter" + className={`${styles.createButton} mt-2`} + />
diff --git a/src/screens/EventVolunteers/Requests/Requests.tsx b/src/screens/EventVolunteers/Requests/Requests.tsx index b19be3d2a0..d8efd92a90 100644 --- a/src/screens/EventVolunteers/Requests/Requests.tsx +++ b/src/screens/EventVolunteers/Requests/Requests.tsx @@ -1,9 +1,9 @@ import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Dropdown, Form } from 'react-bootstrap'; +import { Button, Form } from 'react-bootstrap'; import { Navigate, useParams } from 'react-router-dom'; import { FaXmark } from 'react-icons/fa6'; -import { Search, Sort, WarningAmberRounded } from '@mui/icons-material'; +import { Search, WarningAmberRounded } from '@mui/icons-material'; import { useMutation, useQuery } from '@apollo/client'; import Loader from 'components/Loader/Loader'; @@ -20,6 +20,7 @@ import dayjs from 'dayjs'; import { UPDATE_VOLUNTEER_MEMBERSHIP } from 'GraphQl/Mutations/EventVolunteerMutation'; import { toast } from 'react-toastify'; import { debounce } from '@mui/material'; +import SortingButton from 'subComponents/SortingButton'; const dataGridStyle = { '&.MuiDataGrid-root .MuiDataGrid-cell:focus-within': { @@ -279,30 +280,18 @@ function requests(): JSX.Element {
- - - - {tCommon('sort')} - - - setSortBy('createdAt_DESC')} - data-testid="createdAt_DESC" - > - {t('latest')} - - setSortBy('createdAt_ASC')} - data-testid="createdAt_ASC" - > - {t('earliest')} - - - + + setSortBy(value as 'createdAt_DESC' | 'createdAt_ASC') + } + dataTestIdPrefix="sort" + buttonLabel={tCommon('sort')} + />
diff --git a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroups.tsx b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroups.tsx index 3c70b1db49..b8577acaac 100644 --- a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroups.tsx +++ b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroups.tsx @@ -1,9 +1,9 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Dropdown, Form } from 'react-bootstrap'; +import { Button, Form } from 'react-bootstrap'; import { Navigate, useParams } from 'react-router-dom'; -import { Search, Sort, WarningAmberRounded } from '@mui/icons-material'; +import { Search, WarningAmberRounded } from '@mui/icons-material'; import { useQuery } from '@apollo/client'; @@ -21,6 +21,7 @@ import { EVENT_VOLUNTEER_GROUP_LIST } from 'GraphQl/Queries/EventVolunteerQuerie import VolunteerGroupModal from './VolunteerGroupModal'; import VolunteerGroupDeleteModal from './VolunteerGroupDeleteModal'; import VolunteerGroupViewModal from './VolunteerGroupViewModal'; +import SortingButton from 'subComponents/SortingButton'; enum ModalState { SAME = 'same', @@ -321,56 +322,29 @@ function volunteerGroups(): JSX.Element {
- - - - {tCommon('searchBy', { item: '' })} - - - setSearchBy('leader')} - data-testid="leader" - > - {t('leader')} - - setSearchBy('group')} - data-testid="group" - > - {t('group')} - - - - - - - {tCommon('sort')} - - - setSortBy('volunteers_DESC')} - data-testid="volunteers_DESC" - > - {t('mostVolunteers')} - - setSortBy('volunteers_ASC')} - data-testid="volunteers_ASC" - > - {t('leastVolunteers')} - - - + setSearchBy(value as 'leader' | 'group')} + dataTestIdPrefix="searchByToggle" + buttonLabel={tCommon('searchBy', { item: '' })} + /> + + setSortBy(value as 'volunteers_DESC' | 'volunteers_ASC') + } + dataTestIdPrefix="sort" + buttonLabel={tCommon('sort')} + />
- - - - {tCommon('sort')} - - - setSortBy('hoursVolunteered_DESC')} - data-testid="hoursVolunteered_DESC" - > - {t('mostHoursVolunteered')} - - setSortBy('hoursVolunteered_ASC')} - data-testid="hoursVolunteered_ASC" - > - {t('leastHoursVolunteered')} - - - - - - - {t('status')} - - - setStatus(VolunteerStatus.All)} - data-testid="statusAll" - > - {tCommon('all')} - - setStatus(VolunteerStatus.Pending)} - data-testid="statusPending" - > - {tCommon('pending')} - - setStatus(VolunteerStatus.Accepted)} - data-testid="statusAccepted" - > - {t('accepted')} - - - + + setSortBy( + value as 'hoursVolunteered_DESC' | 'hoursVolunteered_ASC', + ) + } + dataTestIdPrefix="sort" + buttonLabel={tCommon('sort')} + /> + + setStatus(value as VolunteerStatus)} + dataTestIdPrefix="filter" + buttonLabel={t('status')} + />
- - - - {tCommon('sort')} - - - setSortBy('amount_ASC')} - data-testid="amount_ASC" - > - {t('lowestAmount')} - - setSortBy('amount_DESC')} - data-testid="amount_DESC" - > - {t('highestAmount')} - - setSortBy('endDate_DESC')} - data-testid="endDate_DESC" - > - {t('latestEndDate')} - - setSortBy('endDate_ASC')} - data-testid="endDate_ASC" - > - {t('earliestEndDate')} - - - + + setSortBy( + value as + | 'amount_ASC' + | 'amount_DESC' + | 'endDate_ASC' + | 'endDate_DESC', + ) + } + dataTestIdPrefix="filter" + buttonLabel={tCommon('sort')} + />
- - - - {tCommon('sort')} - - - setSortBy('hours_DESC')} - data-testid="hours_DESC" - > - {t('mostHours')} - - setSortBy('hours_ASC')} - data-testid="hours_ASC" - > - {t('leastHours')} - - - - - - - {t('timeFrame')} - - - setTimeFrame(TimeFrame.All)} - data-testid="timeFrameAll" - > - {t('allTime')} - - setTimeFrame(TimeFrame.Weekly)} - data-testid="timeFrameWeekly" - > - {t('weekly')} - - setTimeFrame(TimeFrame.Monthly)} - data-testid="timeFrameMonthly" - > - {t('monthly')} - - setTimeFrame(TimeFrame.Yearly)} - data-testid="timeFrameYearly" - > - {t('yearly')} - - - + + setSortBy(value as 'hours_DESC' | 'hours_ASC') + } + dataTestIdPrefix="sort" + buttonLabel={tCommon('sort')} + /> + setTimeFrame(value as TimeFrame)} + dataTestIdPrefix="timeFrame" + buttonLabel={t('timeFrame')} + type="filter" + />
diff --git a/src/screens/ManageTag/ManageTag.spec.tsx b/src/screens/ManageTag/ManageTag.spec.tsx index 5d86ed3c17..03c7eea393 100644 --- a/src/screens/ManageTag/ManageTag.spec.tsx +++ b/src/screens/ManageTag/ManageTag.spec.tsx @@ -50,7 +50,6 @@ vi.mock('react-toastify', () => ({ }, })); -/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports */ vi.mock('../../components/AddPeopleToTag/AddPeopleToTag', async () => { return await import('./ManageTagMockComponents/MockAddPeopleToTag'); }); @@ -58,7 +57,6 @@ vi.mock('../../components/AddPeopleToTag/AddPeopleToTag', async () => { vi.mock('../../components/TagActions/TagActions', async () => { return await import('./ManageTagMockComponents/MockTagActions'); }); -/* eslint-enable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports */ const renderManageTag = (link: ApolloLink): RenderResult => { return render( @@ -372,9 +370,9 @@ describe('Manage Tag Page', () => { userEvent.click(screen.getByTestId('sortPeople')); await waitFor(() => { - expect(screen.getByTestId('oldest')).toBeInTheDocument(); + expect(screen.getByTestId('ASCENDING')).toBeInTheDocument(); }); - userEvent.click(screen.getByTestId('oldest')); + userEvent.click(screen.getByTestId('ASCENDING')); // returns the tags in reverse order await waitFor(() => { @@ -389,9 +387,9 @@ describe('Manage Tag Page', () => { userEvent.click(screen.getByTestId('sortPeople')); await waitFor(() => { - expect(screen.getByTestId('latest')).toBeInTheDocument(); + expect(screen.getByTestId('DESCENDING')).toBeInTheDocument(); }); - userEvent.click(screen.getByTestId('latest')); + userEvent.click(screen.getByTestId('DESCENDING')); // reverse the order again await waitFor(() => { diff --git a/src/screens/ManageTag/ManageTag.tsx b/src/screens/ManageTag/ManageTag.tsx index 38466f6f11..0832b2ab3e 100644 --- a/src/screens/ManageTag/ManageTag.tsx +++ b/src/screens/ManageTag/ManageTag.tsx @@ -2,13 +2,11 @@ import type { FormEvent } from 'react'; import React, { useEffect, useState } from 'react'; import { useMutation, useQuery } from '@apollo/client'; import { Search, WarningAmberRounded } from '@mui/icons-material'; -import SortIcon from '@mui/icons-material/Sort'; import Loader from 'components/Loader/Loader'; import IconComponent from 'components/IconComponent/IconComponent'; import { useNavigate, useParams, Link } from 'react-router-dom'; import { Col, Form } from 'react-bootstrap'; import Button from 'react-bootstrap/Button'; -import Dropdown from 'react-bootstrap/Dropdown'; import Row from 'react-bootstrap/Row'; import { useTranslation } from 'react-i18next'; import { toast } from 'react-toastify'; @@ -39,6 +37,7 @@ import InfiniteScrollLoader from 'components/InfiniteScrollLoader/InfiniteScroll import EditUserTagModal from './EditUserTagModal'; import RemoveUserTagModal from './RemoveUserTagModal'; import UnassignUserTagModal from './UnassignUserTagModal'; +import SortingButton from 'subComponents/SortingButton'; /** * Component that renders the Manage Tag screen when the app navigates to '/orgtags/:orgId/manageTag/:tagId'. @@ -378,36 +377,19 @@ function ManageTag(): JSX.Element {
- + sortingOptions={[ + { label: tCommon('Latest'), value: 'DESCENDING' }, + { label: tCommon('Oldest'), value: 'ASCENDING' }, + ]} + selectedOption={assignedMemberSortOrder} + onSortChange={(value) => + setAssignedMemberSortOrder(value as SortedByType) + } + dataTestIdPrefix="sortPeople" + buttonLabel={tCommon('sort')} + />
-
- -
+ {superAdmin && (
- - + sortingOptions={[ + { label: t('Latest'), value: 'latest' }, + { label: t('Oldest'), value: 'oldest' }, + ]} + selectedOption={sortingOption} + onSortChange={handleSorting} + dataTestIdPrefix="sortpost" + dropdownTestId="sort" + className={`${styles.dropdown} `} + buttonLabel={t('sortPost')} + />
))}
- - {/* Dropdown menu for selecting settings category */} - - - {t(tab)} - - - {/* Render dropdown items for each settings category */} - {settingtabs.map((setting, index) => ( - setTab(setting)} - className={tab === setting ? 'text-secondary' : ''} - > - {t(setting)} - - ))} - - diff --git a/src/screens/OrganizationActionItems/OrganizationActionItems.spec.tsx b/src/screens/OrganizationActionItems/OrganizationActionItems.spec.tsx index 7ae0fc58eb..89bcc5d824 100644 --- a/src/screens/OrganizationActionItems/OrganizationActionItems.spec.tsx +++ b/src/screens/OrganizationActionItems/OrganizationActionItems.spec.tsx @@ -252,11 +252,11 @@ describe('Testing Organization Action Items Screen', () => { }); await waitFor(() => { - expect(screen.getByTestId('statusAll')).toBeInTheDocument(); + expect(screen.getByTestId('all')).toBeInTheDocument(); }); await act(() => { - fireEvent.click(screen.getByTestId('statusAll')); + fireEvent.click(screen.getByTestId('all')); }); await waitFor(() => { @@ -269,11 +269,11 @@ describe('Testing Organization Action Items Screen', () => { }); await waitFor(() => { - expect(screen.getByTestId('statusPending')).toBeInTheDocument(); + expect(screen.getByTestId('pending')).toBeInTheDocument(); }); await act(() => { - fireEvent.click(screen.getByTestId('statusPending')); + fireEvent.click(screen.getByTestId('pending')); }); await waitFor(() => { @@ -314,11 +314,11 @@ describe('Testing Organization Action Items Screen', () => { }); await waitFor(() => { - expect(screen.getByTestId('statusCompleted')).toBeInTheDocument(); + expect(screen.getByTestId('completed')).toBeInTheDocument(); }); await act(() => { - fireEvent.click(screen.getByTestId('statusCompleted')); + fireEvent.click(screen.getByTestId('completed')); }); await waitFor(() => { diff --git a/src/screens/OrganizationActionItems/OrganizationActionItems.tsx b/src/screens/OrganizationActionItems/OrganizationActionItems.tsx index 6061ba7e7d..e3d55648b0 100644 --- a/src/screens/OrganizationActionItems/OrganizationActionItems.tsx +++ b/src/screens/OrganizationActionItems/OrganizationActionItems.tsx @@ -1,15 +1,9 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Dropdown, Form } from 'react-bootstrap'; +import { Button, Form } from 'react-bootstrap'; import { Navigate, useParams } from 'react-router-dom'; -import { - Circle, - FilterAltOutlined, - Search, - Sort, - WarningAmberRounded, -} from '@mui/icons-material'; +import { Circle, Search, WarningAmberRounded } from '@mui/icons-material'; import dayjs from 'dayjs'; import { useQuery } from '@apollo/client'; @@ -32,6 +26,7 @@ import ItemModal from './ItemModal'; import ItemDeleteModal from './ItemDeleteModal'; import Avatar from 'components/Avatar/Avatar'; import ItemUpdateStatusModal from './ItemUpdateStatusModal'; +import SortingButton from 'subComponents/SortingButton'; enum ItemStatus { Pending = 'pending', @@ -141,6 +136,11 @@ function organizationActionItems(): JSX.Element { [], ); + // Trigger refetch on sortBy or status change + useEffect(() => { + actionItemsRefetch(); + }, [sortBy, status, actionItemsRefetch]); + if (actionItemsLoading) { return ; } @@ -388,89 +388,57 @@ function organizationActionItems(): JSX.Element {
-
- - - - {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')} - - - - - - - {t('status')} - - - setStatus(null)} - data-testid="statusAll" - > - {tCommon('all')} - - setStatus(ItemStatus.Pending)} - data-testid="statusPending" - > - {tCommon('pending')} - - setStatus(ItemStatus.Completed)} - data-testid="statusCompleted" - > - {tCommon('completed')} - - - -
+ + setSearchBy(value as 'assignee' | 'category') + } + dataTestIdPrefix="searchByToggle" + buttonLabel={tCommon('searchBy', { item: '' })} + className={styles.dropdown} // Pass a custom class name if needed + /> + + setSortBy(value as 'dueDate_DESC' | 'dueDate_ASC') + } + dataTestIdPrefix="sort" + buttonLabel={tCommon('sort')} + className={styles.dropdown} // Pass a custom class name if needed + /> + + setStatus(value === 'all' ? null : (value as ItemStatus)) + } + dataTestIdPrefix="filter" + buttonLabel={t('status')} + className={styles.dropdown} // Pass a custom class name if needed + />
- - - - {tCommon('sort')} - - - setSortBy('fundingGoal_ASC')} - data-testid="fundingGoal_ASC" - > - {t('lowestGoal')} - - setSortBy('fundingGoal_DESC')} - data-testid="fundingGoal_DESC" - > - {t('highestGoal')} - - setSortBy('endDate_DESC')} - data-testid="endDate_DESC" - > - {t('latestEndDate')} - - setSortBy('endDate_ASC')} - data-testid="endDate_ASC" - > - {t('earliestEndDate')} - - - + + setSortBy( + value as + | 'fundingGoal_ASC' + | 'fundingGoal_DESC' + | 'endDate_ASC' + | 'endDate_DESC', + ) + } + dataTestIdPrefix="filter" + buttonLabel={tCommon('sort')} + />
-
- - - - {tCommon('sort')} - - - setSortBy('createdAt_DESC')} - data-testid="createdAt_DESC" - > - {t('createdLatest')} - - setSortBy('createdAt_ASC')} - data-testid="createdAt_ASC" - > - {t('createdEarliest')} - - - -
+ + setSortBy(value as 'createdAt_DESC' | 'createdAt_ASC') + } + dataTestIdPrefix="filter" + buttonLabel={tCommon('sort')} + />
- - - - {t('sort')} - - - { - setState(2); - }} - > - - {tCommon('users')} - - - { - setState(0); - }} - > - - {tCommon('members')} - - - { - setState(1); - }} - > - - {tCommon('admins')} - - - - +
- +
diff --git a/src/screens/OrganizationTags/OrganizationTags.tsx b/src/screens/OrganizationTags/OrganizationTags.tsx index 558eb4eaf8..0b233cfaef 100644 --- a/src/screens/OrganizationTags/OrganizationTags.tsx +++ b/src/screens/OrganizationTags/OrganizationTags.tsx @@ -1,13 +1,11 @@ import { useMutation, useQuery } from '@apollo/client'; import { WarningAmberRounded } from '@mui/icons-material'; -import SortIcon from '@mui/icons-material/Sort'; import Loader from 'components/Loader/Loader'; import { useNavigate, useParams, Link } from 'react-router-dom'; import type { ChangeEvent } from 'react'; import React, { useEffect, useState } from 'react'; import { Form } from 'react-bootstrap'; import Button from 'react-bootstrap/Button'; -import Dropdown from 'react-bootstrap/Dropdown'; import Modal from 'react-bootstrap/Modal'; import Row from 'react-bootstrap/Row'; import { useTranslation } from 'react-i18next'; @@ -30,7 +28,7 @@ import { ORGANIZATION_USER_TAGS_LIST } from 'GraphQl/Queries/OrganizationQueries import { CREATE_USER_TAG } from 'GraphQl/Mutations/TagMutations'; import InfiniteScroll from 'react-infinite-scroll-component'; import InfiniteScrollLoader from 'components/InfiniteScrollLoader/InfiniteScrollLoader'; - +import SortingButton from 'subComponents/SortingButton'; /** * Component that renders the Organization Tags screen when the app navigates to '/orgtags/:orgId'. * @@ -294,6 +292,10 @@ function OrganizationTags(): JSX.Element { }, ]; + const handleSortChange = (value: string): void => { + setTagSortOrder(value === 'latest' ? 'DESCENDING' : 'ASCENDING'); + }; + return ( <> @@ -312,40 +314,24 @@ function OrganizationTags(): JSX.Element { />
- + : tCommon('Oldest') + } + onSortChange={handleSortChange} + dataTestIdPrefix="sortTags" + className={styles.dropdown} + />
-
-
- - -
+
+ +
- - - - {tagSortOrder === 'DESCENDING' - ? tCommon('Latest') - : tCommon('Oldest')} - - - setTagSortOrder('DESCENDING')} - > - {tCommon('Latest')} - - setTagSortOrder('ASCENDING')} - > - {tCommon('Oldest')} - - - + setTagSortOrder(value as SortedByType)} + dataTestIdPrefix="sortTags" + buttonLabel={tCommon('sort')} + />
- {/* Dropdown menu for sorting campaigns */} - - - - {tCommon('sort')} - - - setSortBy('fundingGoal_ASC')} - data-testid="fundingGoal_ASC" - > - {t('lowestGoal')} - - setSortBy('fundingGoal_DESC')} - data-testid="fundingGoal_DESC" - > - {t('highestGoal')} - - setSortBy('endDate_DESC')} - data-testid="endDate_DESC" - > - {t('latestEndDate')} - - setSortBy('endDate_ASC')} - data-testid="endDate_ASC" - > - {t('earliestEndDate')} - - - + + setSortBy( + value as + | 'fundingGoal_ASC' + | 'fundingGoal_DESC' + | 'endDate_ASC' + | 'endDate_DESC', + ) + } + dataTestIdPrefix="filter" + buttonLabel={tCommon('sort')} + />
{/* Button to navigate to the user's pledges */} diff --git a/src/screens/UserPortal/Pledges/Pledges.tsx b/src/screens/UserPortal/Pledges/Pledges.tsx index 33e8bf63c2..2ab8214265 100644 --- a/src/screens/UserPortal/Pledges/Pledges.tsx +++ b/src/screens/UserPortal/Pledges/Pledges.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { Dropdown, Form, Button, ProgressBar } from 'react-bootstrap'; +import { Form, Button, ProgressBar } from 'react-bootstrap'; import styles from './Pledges.module.css'; import { useTranslation } from 'react-i18next'; -import { Search, Sort, WarningAmberRounded } from '@mui/icons-material'; +import { Search, WarningAmberRounded } from '@mui/icons-material'; import useLocalStorage from 'utils/useLocalstorage'; import type { InterfacePledgeInfo, InterfaceUserInfo } from 'utils/interfaces'; import { Unstable_Popup as BasePopup } from '@mui/base/Unstable_Popup'; @@ -21,6 +21,7 @@ import { currencySymbols } from 'utils/currency'; import PledgeDeleteModal from 'screens/FundCampaignPledge/PledgeDeleteModal'; import { Navigate, useParams } from 'react-router-dom'; import PledgeModal from '../Campaigns/PledgeModal'; +import SortingButton from 'subComponents/SortingButton'; const dataGridStyle = { '&.MuiDataGrid-root .MuiDataGrid-cell:focus-within': { @@ -393,75 +394,40 @@ const Pledges = (): JSX.Element => {
-
- +
+ + setSearchBy(value as 'pledgers' | 'campaigns') + } + dataTestIdPrefix="searchByDrpdwn" + buttonLabel={t('searchBy')} + /> - - - - {tCommon('sort')} - - - setSortBy('amount_ASC')} - data-testid="amount_ASC" - > - {t('lowestAmount')} - - setSortBy('amount_DESC')} - data-testid="amount_DESC" - > - {t('highestAmount')} - - setSortBy('endDate_DESC')} - data-testid="endDate_DESC" - > - {t('latestEndDate')} - - setSortBy('endDate_ASC')} - data-testid="endDate_ASC" - > - {t('earliestEndDate')} - - - + + setSortBy( + value as + | 'amount_ASC' + | 'amount_DESC' + | 'endDate_ASC' + | 'endDate_DESC', + ) + } + dataTestIdPrefix="filter" + buttonLabel={tCommon('sort')} + />
diff --git a/src/screens/UserPortal/Volunteer/Actions/Actions.tsx b/src/screens/UserPortal/Volunteer/Actions/Actions.tsx index 9bc23969c2..9fc2c44884 100644 --- a/src/screens/UserPortal/Volunteer/Actions/Actions.tsx +++ b/src/screens/UserPortal/Volunteer/Actions/Actions.tsx @@ -1,9 +1,9 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Dropdown, Form } from 'react-bootstrap'; +import { Button, Form } from 'react-bootstrap'; import { Navigate, useParams } from 'react-router-dom'; -import { Circle, Search, Sort, WarningAmberRounded } from '@mui/icons-material'; +import { Circle, Search, WarningAmberRounded } from '@mui/icons-material'; import dayjs from 'dayjs'; import { useQuery } from '@apollo/client'; @@ -22,6 +22,7 @@ import Avatar from 'components/Avatar/Avatar'; import ItemUpdateStatusModal from 'screens/OrganizationActionItems/ItemUpdateStatusModal'; import { ACTION_ITEMS_BY_USER } from 'GraphQl/Queries/ActionItemQueries'; import useLocalStorage from 'utils/useLocalstorage'; +import SortingButton from 'subComponents/SortingButton'; enum ModalState { VIEW = 'view', @@ -373,54 +374,29 @@ function actions(): JSX.Element {
- - - - {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')} - - - + + setSearchBy(value as 'assignee' | 'category') + } + dataTestIdPrefix="searchByToggle" + buttonLabel={tCommon('searchBy', { item: '' })} + /> + + setSortBy(value as 'dueDate_DESC' | 'dueDate_ASC') + } + dataTestIdPrefix="sort" + buttonLabel={tCommon('sort')} + />
diff --git a/src/screens/UserPortal/Volunteer/Groups/Groups.tsx b/src/screens/UserPortal/Volunteer/Groups/Groups.tsx index 160dc0b23a..4cd2470010 100644 --- a/src/screens/UserPortal/Volunteer/Groups/Groups.tsx +++ b/src/screens/UserPortal/Volunteer/Groups/Groups.tsx @@ -1,11 +1,10 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Dropdown, Form } from 'react-bootstrap'; +import { Button, Form } from 'react-bootstrap'; import { Navigate, useParams } from 'react-router-dom'; - -import { Search, Sort, WarningAmberRounded } from '@mui/icons-material'; - +import { Search, WarningAmberRounded } from '@mui/icons-material'; import { useQuery } from '@apollo/client'; +import { debounce, Stack } from '@mui/material'; import type { InterfaceVolunteerGroupInfo } from 'utils/interfaces'; import Loader from 'components/Loader/Loader'; @@ -14,13 +13,13 @@ import { type GridCellParams, type GridColDef, } from '@mui/x-data-grid'; -import { debounce, Stack } from '@mui/material'; import Avatar from 'components/Avatar/Avatar'; import styles from '../../../../style/app.module.css'; import { EVENT_VOLUNTEER_GROUP_LIST } from 'GraphQl/Queries/EventVolunteerQueries'; import VolunteerGroupViewModal from 'screens/EventVolunteers/VolunteerGroups/VolunteerGroupViewModal'; import useLocalStorage from 'utils/useLocalstorage'; import GroupModal from './GroupModal'; +import SortingButton from 'subComponents/SortingButton'; enum ModalState { EDIT = 'edit', @@ -313,56 +312,27 @@ function groups(): JSX.Element {
- - - - {tCommon('searchBy', { item: '' })} - - - setSearchBy('leader')} - data-testid="leader" - > - {t('leader')} - - setSearchBy('group')} - data-testid="group" - > - {t('group')} - - - - - - - {tCommon('sort')} - - - setSortBy('volunteers_DESC')} - data-testid="volunteers_DESC" - > - {t('mostVolunteers')} - - setSortBy('volunteers_ASC')} - data-testid="volunteers_ASC" - > - {t('leastVolunteers')} - - - + setSearchBy(value as 'leader' | 'group')} + dataTestIdPrefix="searchByToggle" + buttonLabel={tCommon('searchBy', { item: '' })} + /> + + setSortBy(value as 'volunteers_DESC' | 'volunteers_ASC') + } + dataTestIdPrefix="sort" + buttonLabel={tCommon('sort')} + />
diff --git a/src/screens/UserPortal/Volunteer/Invitations/Invitations.spec.tsx b/src/screens/UserPortal/Volunteer/Invitations/Invitations.spec.tsx index 867f95c1aa..2c8d0835ca 100644 --- a/src/screens/UserPortal/Volunteer/Invitations/Invitations.spec.tsx +++ b/src/screens/UserPortal/Volunteer/Invitations/Invitations.spec.tsx @@ -171,7 +171,7 @@ describe('Testing Invvitations Screen', () => { expect(filter).toBeInTheDocument(); fireEvent.click(filter); - const filterAll = await screen.findByTestId('filterAll'); + const filterAll = await screen.findByTestId('all'); expect(filterAll).toBeInTheDocument(); fireEvent.click(filterAll); @@ -189,7 +189,7 @@ describe('Testing Invvitations Screen', () => { expect(filter).toBeInTheDocument(); fireEvent.click(filter); - const filterGroup = await screen.findByTestId('filterGroup'); + const filterGroup = await screen.findByTestId('group'); expect(filterGroup).toBeInTheDocument(); fireEvent.click(filterGroup); @@ -210,7 +210,7 @@ describe('Testing Invvitations Screen', () => { expect(filter).toBeInTheDocument(); fireEvent.click(filter); - const filterIndividual = await screen.findByTestId('filterIndividual'); + const filterIndividual = await screen.findByTestId('individual'); expect(filterIndividual).toBeInTheDocument(); fireEvent.click(filterIndividual); diff --git a/src/screens/UserPortal/Volunteer/Invitations/Invitations.tsx b/src/screens/UserPortal/Volunteer/Invitations/Invitations.tsx index a79b64251d..35dbe67264 100644 --- a/src/screens/UserPortal/Volunteer/Invitations/Invitations.tsx +++ b/src/screens/UserPortal/Volunteer/Invitations/Invitations.tsx @@ -1,14 +1,9 @@ import React, { useMemo, useState } from 'react'; -import { Dropdown, Form, Button } from 'react-bootstrap'; +import { Form, Button } from 'react-bootstrap'; import styles from '../VolunteerManagement.module.css'; import { useTranslation } from 'react-i18next'; import { Navigate, useParams } from 'react-router-dom'; -import { - FilterAltOutlined, - Search, - Sort, - WarningAmberRounded, -} from '@mui/icons-material'; +import { Search, WarningAmberRounded } from '@mui/icons-material'; import { TbCalendarEvent } from 'react-icons/tb'; import { FaUserGroup } from 'react-icons/fa6'; import { debounce, Stack } from '@mui/material'; @@ -21,6 +16,7 @@ import Loader from 'components/Loader/Loader'; import { USER_VOLUNTEER_MEMBERSHIP } from 'GraphQl/Queries/EventVolunteerQueries'; import { UPDATE_VOLUNTEER_MEMBERSHIP } from 'GraphQl/Mutations/EventVolunteerMutation'; import { toast } from 'react-toastify'; +import SortingButton from 'subComponents/SortingButton'; enum ItemFilter { Group = 'group', @@ -120,7 +116,7 @@ const Invitations = (): JSX.Element => { // loads the invitations when the component mounts if (invitationLoading) return ; if (invitationError) { - // Displays an error message if there is an issue loading the invvitations + // Displays an error message if there is an issue loading the invitations return (
@@ -162,63 +158,30 @@ const Invitations = (): JSX.Element => {
- {/* Dropdown menu for sorting invitations */} - - - - {tCommon('sort')} - - - setSortBy('createdAt_DESC')} - data-testid="createdAt_DESC" - > - {t('receivedLatest')} - - setSortBy('createdAt_ASC')} - data-testid="createdAt_ASC" - > - {t('receivedEarliest')} - - - - - - - - {t('filter')} - - - setFilter(null)} - data-testid="filterAll" - > - {tCommon('all')} - - setFilter(ItemFilter.Group)} - data-testid="filterGroup" - > - {t('groupInvite')} - - setFilter(ItemFilter.Individual)} - data-testid="filterIndividual" - > - {t('individualInvite')} - - - + + setSortBy(value as 'createdAt_DESC' | 'createdAt_ASC') + } + dataTestIdPrefix="sort" + buttonLabel={tCommon('sort')} + /> + + setFilter(value === 'all' ? null : (value as ItemFilter)) + } + dataTestIdPrefix="filter" + buttonLabel={t('filter')} + type="filter" + />
diff --git a/src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.tsx b/src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.tsx index bd61ca97e0..eecb874210 100644 --- a/src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.tsx +++ b/src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useState } from 'react'; -import { Dropdown, Form, Button } from 'react-bootstrap'; +import { Form, Button } from 'react-bootstrap'; import styles from '../VolunteerManagement.module.css'; import { useTranslation } from 'react-i18next'; import { Navigate, useParams } from 'react-router-dom'; @@ -19,7 +19,7 @@ import { Stack, debounce, } from '@mui/material'; -import { Circle, Search, Sort, WarningAmberRounded } from '@mui/icons-material'; +import { Circle, Search, WarningAmberRounded } from '@mui/icons-material'; import { GridExpandMoreIcon } from '@mui/x-data-grid'; import useLocalStorage from 'utils/useLocalstorage'; @@ -31,6 +31,7 @@ import { USER_EVENTS_VOLUNTEER } from 'GraphQl/Queries/PlugInQueries'; import { CREATE_VOLUNTEER_MEMBERSHIP } from 'GraphQl/Mutations/EventVolunteerMutation'; import { toast } from 'react-toastify'; import { FaCheck } from 'react-icons/fa'; +import SortingButton from 'subComponents/SortingButton'; /** * The `UpcomingEvents` component displays list of upcoming events for the user to volunteer. @@ -90,7 +91,7 @@ const UpcomingEvents = (): JSX.Element => { } }; - // Fetches upcomin events based on the organization ID, search term, and sorting order + // Fetches upcoming events based on the organization ID, search term, and sorting order const { data: eventsData, loading: eventsLoading, @@ -169,31 +170,18 @@ const UpcomingEvents = (): JSX.Element => {
- - - - {tCommon('searchBy', { item: '' })} - - - setSearchBy('title')} - data-testid="title" - > - {t('name')} - - setSearchBy('location')} - data-testid="location" - > - {tCommon('location')} - - - + + setSearchBy(value as 'title' | 'location') + } + dataTestIdPrefix="searchByToggle" + buttonLabel={tCommon('searchBy', { item: '' })} + />
diff --git a/src/screens/Users/Users.tsx b/src/screens/Users/Users.tsx index 936807f1ec..ef9f001f4d 100644 --- a/src/screens/Users/Users.tsx +++ b/src/screens/Users/Users.tsx @@ -1,13 +1,11 @@ import { useQuery } from '@apollo/client'; import React, { useEffect, useState } from 'react'; -import { Dropdown, Form, Table } from 'react-bootstrap'; +import { Form, Table } from 'react-bootstrap'; import Button from 'react-bootstrap/Button'; import { useTranslation } from 'react-i18next'; import { toast } from 'react-toastify'; import { Search } from '@mui/icons-material'; -import FilterListIcon from '@mui/icons-material/FilterList'; -import SortIcon from '@mui/icons-material/Sort'; import { ORGANIZATION_CONNECTION_LIST, USER_LIST, @@ -19,6 +17,8 @@ import type { InterfaceQueryUserListItem } from 'utils/interfaces'; import styles from '../../style/app.module.css'; import useLocalStorage from 'utils/useLocalstorage'; import type { ApolloError } from '@apollo/client'; +import SortingButton from 'subComponents/SortingButton'; + /** * The `Users` component is responsible for displaying a list of users in a paginated and sortable format. * It supports search functionality, filtering, and sorting of users. The component integrates with GraphQL @@ -372,74 +372,29 @@ const Users = (): JSX.Element => {
- - + +
diff --git a/src/style/app.module.css b/src/style/app.module.css index b016e9aaaf..54c6324bcb 100644 --- a/src/style/app.module.css +++ b/src/style/app.module.css @@ -273,13 +273,13 @@ } .dropdown { - background-color: var(--bs-white); + background-color: var(--bs-white) !important; border: 1px solid var(--brown-color); - color: var(--brown-color); + color: var(--brown-color) !important; position: relative; display: inline-block; - margin-top: 10px; - margin-bottom: 10px; + /* margin-top: 10px; + margin-bottom: 10px; */ } .dropdown:is(:hover, :focus, :active, :focus-visible, .show) { @@ -1769,14 +1769,6 @@ input[type='radio']:checked + label:hover { box-shadow: 0 1px 1px var(--brand-primary); } -.dropdowns { - background-color: var(--bs-white); - border: 1px solid var(--light-green); - position: relative; - display: inline-block; - color: var(--light-green); -} - .chipIcon { height: 0.9rem !important; } diff --git a/src/subComponents/SortingButton.tsx b/src/subComponents/SortingButton.tsx new file mode 100644 index 0000000000..7ce7703d39 --- /dev/null +++ b/src/subComponents/SortingButton.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import { Dropdown } from 'react-bootstrap'; +import SortIcon from '@mui/icons-material/Sort'; +import FilterAltOutlined from '@mui/icons-material/FilterAltOutlined'; +import PropTypes from 'prop-types'; +import styles from '../style/app.module.css'; + +interface InterfaceSortingOption { + /** The label to display for the sorting option */ + label: string; + /** The value associated with the sorting option */ + value: string; +} + +interface InterfaceSortingButtonProps { + /** The title attribute for the Dropdown */ + title?: string; + /** The list of sorting options to display in the Dropdown */ + sortingOptions: InterfaceSortingOption[]; + /** The currently selected sorting option */ + selectedOption?: string; + /** Callback function to handle sorting option change */ + onSortChange: (value: string) => void; + /** The prefix for data-testid attributes for testing */ + dataTestIdPrefix: string; + /** The data-testid attribute for the Dropdown */ + dropdownTestId?: string; + /** Custom class name for the Dropdown */ + className?: string; + /** Optional prop for custom button label */ + buttonLabel?: string; + /** Type to determine the icon to display: 'sort' or 'filter' */ + type?: 'sort' | 'filter'; +} + +/** + * SortingButton component renders a Dropdown with sorting options. + * It allows users to select a sorting option and triggers a callback on selection. + * + * @param props - The properties for the SortingButton component. + * @returns The rendered SortingButton component. + */ +const SortingButton: React.FC = ({ + title, + sortingOptions, + selectedOption, + onSortChange, + dataTestIdPrefix, + dropdownTestId, + className = styles.dropdown, + buttonLabel, + type = 'sort', +}) => { + // Determine the icon based on the type + const IconComponent = type === 'filter' ? FilterAltOutlined : SortIcon; + + return ( + + ); +}; + +SortingButton.propTypes = { + title: PropTypes.string, + sortingOptions: PropTypes.arrayOf( + PropTypes.exact({ + label: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + }).isRequired, + ).isRequired, + selectedOption: PropTypes.string, + onSortChange: PropTypes.func.isRequired, + dataTestIdPrefix: PropTypes.string.isRequired, + dropdownTestId: PropTypes.string, + buttonLabel: PropTypes.string, // Optional prop for custom button label + type: PropTypes.oneOf(['sort', 'filter']), // Type to determine the icon +}; + +export default SortingButton; From 3314e4e68da4edd59e8201a0763ffa857662e18e Mon Sep 17 00:00:00 2001 From: Aadhil Ahamed Date: Fri, 10 Jan 2025 02:01:37 +0530 Subject: [PATCH 20/32] Fixed Initial Left Drawer Visibility (#3223) * fixed Left Drawer Visibility * fix css check error --- src/components/LeftDrawer/LeftDrawer.tsx | 8 +++++++- src/screens/ForgotPassword/ForgotPassword.tsx | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/LeftDrawer/LeftDrawer.tsx b/src/components/LeftDrawer/LeftDrawer.tsx index eabf9722f8..1ef8192ae1 100644 --- a/src/components/LeftDrawer/LeftDrawer.tsx +++ b/src/components/LeftDrawer/LeftDrawer.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import Button from 'react-bootstrap/Button'; import { useTranslation } from 'react-i18next'; import { NavLink } from 'react-router-dom'; @@ -31,6 +31,12 @@ const leftDrawer = ({ const { getItem } = useLocalStorage(); const superAdmin = getItem('SuperAdmin'); + useEffect(() => { + if (hideDrawer === null) { + setHideDrawer(false); + } + }, []); + /** * Handles link click to hide the drawer on smaller screens. */ diff --git a/src/screens/ForgotPassword/ForgotPassword.tsx b/src/screens/ForgotPassword/ForgotPassword.tsx index 00fdb77c3b..49c0a2af6e 100644 --- a/src/screens/ForgotPassword/ForgotPassword.tsx +++ b/src/screens/ForgotPassword/ForgotPassword.tsx @@ -16,7 +16,7 @@ import { Form } from 'react-bootstrap'; import Button from 'react-bootstrap/Button'; import { useTranslation } from 'react-i18next'; import { errorHandler } from 'utils/errorHandler'; -import styles from 'style/app.module.css'; +import styles from '../../style/app.module.css'; import useLocalStorage from 'utils/useLocalstorage'; /** From 024f421f2d3c1ed1a4dcf6938fe69f505cf1ead5 Mon Sep 17 00:00:00 2001 From: Aayush Goyal <138467261+Aayushgoyal00@users.noreply.github.com> Date: Fri, 10 Jan 2025 02:06:12 +0530 Subject: [PATCH 21/32] style: enhance AverageRating component with custom styles and CSS variables (#3213) --- .../EventStats/Statistics/AverageRating.tsx | 14 +++++--------- src/style/app.module.css | 13 ++++++++++++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/components/EventStats/Statistics/AverageRating.tsx b/src/components/EventStats/Statistics/AverageRating.tsx index 9f1a157e01..f2e22338ec 100644 --- a/src/components/EventStats/Statistics/AverageRating.tsx +++ b/src/components/EventStats/Statistics/AverageRating.tsx @@ -4,7 +4,7 @@ import Rating from '@mui/material/Rating'; import FavoriteIcon from '@mui/icons-material/Favorite'; import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; import Typography from '@mui/material/Typography'; - +import styles from '../../../style/app.module.css'; // Props for the AverageRating component type ModalPropType = { data: { @@ -33,7 +33,7 @@ type FeedbackType = { export const AverageRating = ({ data }: ModalPropType): JSX.Element => { return ( <> - +

Average Review Score

@@ -50,13 +50,9 @@ export const AverageRating = ({ data }: ModalPropType): JSX.Element => { icon={} size="medium" emptyIcon={} - sx={{ - '& .MuiRating-iconFilled': { - color: '#ff6d75', // Color for filled stars - }, - '& .MuiRating-iconHover': { - color: '#ff3d47', // Color for star on hover - }, + classes={{ + iconFilled: styles.ratingFilled, + iconHover: styles.ratingHover, }} />
diff --git a/src/style/app.module.css b/src/style/app.module.css index 54c6324bcb..d9645b2bdd 100644 --- a/src/style/app.module.css +++ b/src/style/app.module.css @@ -48,7 +48,8 @@ --subtle-blue-grey-hover: #5f7e91; --white: #fff; --black: black; - + --rating-star-filled: #ff6d75; + --rating-star-hover: #ff3d47; /* Background and Border */ --table-bg: #eaebef; --tablerow-bg: #eff1f7; @@ -942,7 +943,17 @@ hr { .card { width: fit-content; } +.cardContainer { + width: 300px; +} +.ratingFilled { + color: var(--rating-star-filled); /* Color for filled stars */ +} + +.ratingHover { + color: var(--rating-star-hover); /* Color for star on hover */ +} .cardHeader { padding: 1.25rem 1rem 1rem 1rem; border-bottom: 1px solid var(--bs-gray-200); From edc013db1d5b65326f845a9d5f014eff22e31bf1 Mon Sep 17 00:00:00 2001 From: NISHANT SINGH <151461374+NishantSinghhhhh@users.noreply.github.com> Date: Fri, 10 Jan 2025 02:12:43 +0530 Subject: [PATCH 22/32] added vitest for StaticMockLinks (#3206) * added vitest for StaticMockLinks Signed-off-by: NishantSinghhhhh * Rabbits changes Signed-off-by: NishantSinghhhhh * Rabbits changes Signed-off-by: NishantSinghhhhh * passing tests Signed-off-by: NishantSinghhhhh * Revert "passing tests" This reverts commit 4f2d5a649e11bb4eb0d25f0ff1477bd862ca2dba. * Revert "Rabbits changes" This reverts commit a92a5553c1933a2c89abbb9adcdaeb3a2bce48f3. * Revert "Rabbits changes" This reverts commit e410ab5c96b4c4a4499059b01e4aaa55bcf24e9b. * Rabbits changes Signed-off-by: NishantSinghhhhh * passing tests Signed-off-by: NishantSinghhhhh * passing tests Signed-off-by: NishantSinghhhhh * 100% coverage Signed-off-by: NishantSinghhhhh * 100% coverage Signed-off-by: NishantSinghhhhh --------- Signed-off-by: NishantSinghhhhh --- src/utils/StaticMockLink.spec.ts | 725 +++++++++++++++++++++++++++++++ 1 file changed, 725 insertions(+) create mode 100644 src/utils/StaticMockLink.spec.ts diff --git a/src/utils/StaticMockLink.spec.ts b/src/utils/StaticMockLink.spec.ts new file mode 100644 index 0000000000..15c5cc3443 --- /dev/null +++ b/src/utils/StaticMockLink.spec.ts @@ -0,0 +1,725 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import { StaticMockLink, mockSingleLink } from './StaticMockLink'; +import type { Observer } from '@apollo/client'; +import type { MockedResponse } from '@apollo/react-testing'; +import { gql, Observable } from '@apollo/client'; +import { print } from 'graphql'; +import type { FetchResult } from '@apollo/client/link/core'; +import { equal } from '@wry/equality'; +class TestableStaticMockLink extends StaticMockLink { + public setErrorHandler( + handler: (error: unknown, observer?: Observer) => false | void, + ): void { + this.onError = handler; + } +} + +const TEST_QUERY = gql` + query TestQuery($id: ID!) { + item(id: $id) { + id + name + } + } +`; +const mockQuery = gql` + query TestQuery { + test { + id + name + } + } +`; +const sampleQuery = gql` + query SampleQuery($id: ID!) { + user(id: $id) { + id + name + } + } +`; + +const sampleResponse = { + data: { + user: { + id: '1', + name: 'Test User', + __typename: 'User', + }, + }, +}; +describe('StaticMockLink', () => { + const sampleQuery = gql` + query SampleQuery($id: ID!) { + user(id: $id) { + id + name + } + } + `; + + const sampleVariables = { id: '1' }; + + const sampleResponse = { + data: { + user: { + id: '1', + name: 'John Doe', + __typename: 'User', + }, + }, + }; + + let mockLink: StaticMockLink; + + beforeEach((): void => { + mockLink = new StaticMockLink([], true); + }); + + test('should create instance with empty mocked responses', () => { + expect(mockLink).toBeInstanceOf(StaticMockLink); + expect(mockLink.addTypename).toBe(true); + }); + + test('should add mocked response', () => { + const mockedResponse = { + request: { + query: sampleQuery, + variables: sampleVariables, + }, + result: sampleResponse, + }; + + mockLink.addMockedResponse(mockedResponse); + // This is Mocked Response + return new Promise((resolve) => { + const observable = mockLink.request({ + query: sampleQuery, + variables: sampleVariables, + }); + + observable?.subscribe({ + next: (response) => { + expect(response).toEqual(sampleResponse); + }, + complete: () => { + resolve(); + }, + }); + }); + }); + + test('should handle delayed responses', () => { + vi.useFakeTimers(); // Start using fake timers + const delay = 100; + const mockedResponse = { + request: { + query: sampleQuery, + variables: sampleVariables, + }, + result: sampleResponse, + delay, + }; + + mockLink.addMockedResponse(mockedResponse); + + let completed = false; + + return new Promise((resolve) => { + const observable = mockLink.request({ + query: sampleQuery, + variables: sampleVariables, + }); + + observable?.subscribe({ + next: (response) => { + expect(response).toEqual(sampleResponse); + completed = true; + }, + complete: () => { + expect(completed).toBe(true); + resolve(); + }, + error: (error) => { + throw error; + }, + }); + + vi.advanceTimersByTime(delay); // Advance time by the delay + }).finally(() => { + vi.useRealTimers(); // Restore real timers + }); + }); + + test('should handle errors in response', () => { + const errorResponse = { + request: { + query: sampleQuery, + variables: sampleVariables, + }, + error: new Error('GraphQL Error'), + }; + + mockLink.addMockedResponse(errorResponse); + + return new Promise((resolve) => { + const observable = mockLink.request({ + query: sampleQuery, + variables: sampleVariables, + }); + + observable?.subscribe({ + error: (error) => { + expect(error.message).toBe('GraphQL Error'); + resolve(); + }, + }); + }); + }); + + test('should handle dynamic results using newData', () => { + const dynamicResponse = { + request: { + query: sampleQuery, + variables: { id: '2' }, // Changed to match the request variables + }, + result: sampleResponse, + newData: (variables: { id: string }) => ({ + data: { + user: { + id: variables.id, + name: `User ${variables.id}`, + __typename: 'User', + }, + }, + }), + }; + + mockLink.addMockedResponse(dynamicResponse); + + return new Promise((resolve) => { + const observable = mockLink.request({ + query: sampleQuery, + variables: { id: '2' }, // Matches the request variables in mocked response + }); + + observable?.subscribe({ + next: (response) => { + expect(response).toEqual({ + data: { + user: { + id: '2', + name: 'User 2', + __typename: 'User', + }, + }, + }); + }, + complete: () => { + resolve(); + }, + error: (error) => { + // Add error handling to help debug test failures + console.error('Test error:', error); + throw error; + }, + }); + }); + }); + test('should error when no matching response is found', () => { + return new Promise((resolve) => { + const observable = mockLink.request({ + query: sampleQuery, + variables: sampleVariables, + }); + + observable?.subscribe({ + error: (error) => { + expect(error.message).toContain( + 'No more mocked responses for the query', + ); + resolve(); + }, + }); + }); + }); +}); + +describe('mockSingleLink', () => { + test('should create StaticMockLink with default typename', () => { + const mockedResponse = { + request: { + query: gql` + query { + hello + } + `, + variables: {}, + }, + result: { data: { hello: 'world' } }, + }; + + const link = mockSingleLink(mockedResponse); + expect(link).toBeInstanceOf(StaticMockLink); + }); + + test('should create StaticMockLink with specified typename setting', () => { + const mockedResponse = { + request: { + query: gql` + query { + hello + } + `, + variables: {}, + }, + result: { data: { hello: 'world' } }, + }; + + const link = mockSingleLink(mockedResponse, false); + expect((link as StaticMockLink).addTypename).toBe(false); + }); + + test('should handle non-matching variables between request and mocked response', () => { + const mockLink = new StaticMockLink([]); + const mockedResponse = { + request: { + query: sampleQuery, + variables: { id: '1' }, + }, + result: sampleResponse, + }; + + mockLink.addMockedResponse(mockedResponse); + + return new Promise((resolve) => { + const observable = mockLink.request({ + query: sampleQuery, + variables: { id: '2' }, // Different variables + }); + + observable?.subscribe({ + error: (error) => { + expect(error.message).toContain('No more mocked responses'); + resolve(); + }, + }); + }); + }); + + test('should handle matching query but mismatched variable structure', () => { + const mockLink = new StaticMockLink([]); + const mockedResponse = { + request: { + query: sampleQuery, + variables: { id: '1', extra: 'field' }, + }, + result: sampleResponse, + }; + + mockLink.addMockedResponse(mockedResponse); + + return new Promise((resolve) => { + const observable = mockLink.request({ + query: sampleQuery, + variables: { id: '1' }, // Missing extra field + }); + + observable?.subscribe({ + error: (error) => { + expect(error.message).toContain('No more mocked responses'); + resolve(); + }, + }); + }); + }); + + test('should handle onError behavior correctly', async () => { + const mockLink = new TestableStaticMockLink([], true); + const handlerSpy = vi.fn().mockReturnValue(undefined); // Return undefined to trigger error throw + + mockLink.setErrorHandler(handlerSpy); + + await new Promise((resolve) => { + const observable = mockLink.request({ + query: gql` + query TestQuery { + field + } + `, + variables: {}, + }); + + observable?.subscribe({ + next: () => { + throw new Error('Should not succeed'); + }, + error: (error) => { + // Verify the error handler was called + expect(handlerSpy).toHaveBeenCalledTimes(1); + + // Verify we got the expected error + expect(error.message).toContain('No more mocked responses'); + + resolve(); + }, + }); + }); + }, 10000); + it('should throw an error if a mocked response lacks result and error', () => { + const mockedResponses = [ + { + request: { query: mockQuery }, + // Missing `result` and `error` + }, + ]; + + const link = new StaticMockLink(mockedResponses); + + const operation = { + query: mockQuery, + variables: {}, + }; + + const observable = link.request(operation); + + expect(observable).toBeInstanceOf(Observable); + + // Subscribe to the observable and expect an error + observable?.subscribe({ + next: () => { + // This shouldn't be called + throw new Error('next() should not be called'); + }, + error: (err) => { + // Check the error message + expect(err.message).toContain( + 'Mocked response should contain either result or error', + ); + }, + complete: () => { + // This shouldn't be called + throw new Error('complete() should not be called'); + }, + }); + }); + + it('should return undefined when no mocked response matches operation variables', () => { + const mockedResponses = [ + { + request: { + query: mockQuery, + variables: { id: '123' }, + }, + result: { data: { test: { id: '123', name: 'Test Name' } } }, + }, + ]; + + const link = new StaticMockLink(mockedResponses); + + // Simulate operation with unmatched variables + const operation = { + query: mockQuery, + variables: { id: '999' }, + }; + + const key = JSON.stringify({ + query: link.addTypename + ? print(mockQuery) // Add typename if necessary + : print(mockQuery), + }); + + const mockedResponsesByKey = link['_mockedResponsesByKey'][key]; + + // Emulate the internal logic + let responseIndex = -1; + const response = (mockedResponsesByKey || []).find((res, index) => { + const requestVariables = operation.variables || {}; + const mockedResponseVariables = res.request.variables || {}; + if (equal(requestVariables, mockedResponseVariables)) { + responseIndex = index; + return true; + } + return false; + }); + + // Assertions + expect(response).toBeUndefined(); + expect(responseIndex).toBe(-1); + }); + + test('should initialize with empty mocked responses array', () => { + // Test with null/undefined + const mockLinkNull = new StaticMockLink( + null as unknown as readonly MockedResponse[], + ); + expect(mockLinkNull).toBeInstanceOf(StaticMockLink); + + // Test with defined responses + const mockResponses: readonly MockedResponse[] = [ + { + request: { + query: sampleQuery, + variables: { id: '1' }, + }, + result: { + data: { + user: { + id: '1', + name: 'Test User', + __typename: 'User', + }, + }, + }, + }, + { + request: { + query: sampleQuery, + variables: { id: '2' }, + }, + result: { + data: { + user: { + id: '2', + name: 'Test User 2', + __typename: 'User', + }, + }, + }, + }, + ]; + + const mockLink = new StaticMockLink(mockResponses, true); + + // Verify responses were added via constructor + const observable1 = mockLink.request({ + query: sampleQuery, + variables: { id: '1' }, + }); + + const observable2 = mockLink.request({ + query: sampleQuery, + variables: { id: '2' }, + }); + + return Promise.all([ + new Promise((resolve) => { + observable1?.subscribe({ + next: (response) => { + expect(response?.data?.user?.id).toBe('1'); + resolve(); + }, + }); + }), + new Promise((resolve) => { + observable2?.subscribe({ + next: (response) => { + expect(response?.data?.user?.id).toBe('2'); + resolve(); + }, + }); + }), + ]); + }); + + test('should handle undefined operation variables', () => { + const mockLink = new StaticMockLink([]); + const mockedResponse: MockedResponse = { + request: { + query: sampleQuery, + }, + result: { + data: { + user: { + id: '1', + name: 'Test User', + __typename: 'User', + }, + }, + }, + }; + + mockLink.addMockedResponse(mockedResponse); + + const observable = mockLink.request({ + query: sampleQuery, + // Intentionally omitting variables + }); + + return new Promise((resolve) => { + observable?.subscribe({ + next: (response) => { + expect(response?.data?.user?.id).toBe('1'); + resolve(); + }, + }); + }); + }); + + test('should handle response with direct result value', async () => { + const mockResponse: MockedResponse = { + request: { + query: TEST_QUERY, + variables: { id: '1' }, + }, + result: { + data: { + item: { + id: '1', + name: 'Test Item', + __typename: 'Item', + }, + }, + }, + }; + + const link = new StaticMockLink([mockResponse]); + + return new Promise((resolve, reject) => { + const observable = link.request({ + query: TEST_QUERY, + variables: { id: '1' }, + }); + + if (!observable) { + reject(new Error('Observable is null')); + return; + } + + observable.subscribe({ + next(response) { + expect(response).toEqual(mockResponse.result); + resolve(); + }, + error: reject, + }); + }); + }); + + test('should handle response with result function', async () => { + const mockResponse: MockedResponse = { + request: { + query: TEST_QUERY, + variables: { id: '1' }, + }, + result: (variables: { id: string }) => ({ + data: { + item: { + id: variables.id, + name: `Test Item ${variables.id}`, + __typename: 'Item', + }, + }, + }), + }; + + const link = new StaticMockLink([mockResponse]); + + return new Promise((resolve, reject) => { + const observable = link.request({ + query: TEST_QUERY, + variables: { id: '1' }, + }); + + if (!observable) { + reject(new Error('Observable is null')); + return; + } + + observable.subscribe({ + next(response) { + expect(response).toEqual({ + data: { + item: { + id: '1', + name: 'Test Item 1', + __typename: 'Item', + }, + }, + }); + resolve(); + }, + error: reject, + }); + }); + }); + + test('should handle response with error', async () => { + const testError = new Error('Test error'); + const mockResponse: MockedResponse = { + request: { + query: TEST_QUERY, + variables: { id: '1' }, + }, + error: testError, + }; + + const link = new StaticMockLink([mockResponse]); + + return new Promise((resolve, reject) => { + const observable = link.request({ + query: TEST_QUERY, + variables: { id: '1' }, + }); + + if (!observable) { + reject(new Error('Observable is null')); + return; + } + + observable.subscribe({ + next() { + reject(new Error('Should not have called next')); + }, + error(error) { + expect(error).toBe(testError); + resolve(); + }, + }); + }); + }); + + test('should respect response delay', async () => { + const mockResponse: MockedResponse = { + request: { + query: TEST_QUERY, + variables: { id: '1' }, + }, + result: { + data: { + item: { + id: '1', + name: 'Test Item', + __typename: 'Item', + }, + }, + }, + delay: 50, + }; + + const link = new StaticMockLink([mockResponse]); + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const observable = link.request({ + query: TEST_QUERY, + variables: { id: '1' }, + }); + + if (!observable) { + reject(new Error('Observable is null')); + return; + } + + observable.subscribe({ + next(response) { + const elapsed = Date.now() - startTime; + expect(elapsed).toBeGreaterThanOrEqual(50); + expect(response).toEqual(mockResponse.result); + resolve(); + }, + error: reject, + }); + }); + }); +}); From e9aec1710ea8a4a61bb0c4daaded5b335ffd637b Mon Sep 17 00:00:00 2001 From: Syed Ali Ul Hasan Date: Fri, 10 Jan 2025 03:41:35 +0530 Subject: [PATCH 23/32] Refactored src/screens/FundCampaignPledge/PledgeDeleteModal.test.tsx from jest to vitest (#3219) * refactored src/screens/FundCampaignPledge/PledgeDeleteModal.test.tsx from jest to vitest * fixed linting issues --- ...eleteModal.test.tsx => PledgeDeleteModal.spec.tsx} | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) rename src/screens/FundCampaignPledge/{PledgeDeleteModal.test.tsx => PledgeDeleteModal.spec.tsx} (95%) diff --git a/src/screens/FundCampaignPledge/PledgeDeleteModal.test.tsx b/src/screens/FundCampaignPledge/PledgeDeleteModal.spec.tsx similarity index 95% rename from src/screens/FundCampaignPledge/PledgeDeleteModal.test.tsx rename to src/screens/FundCampaignPledge/PledgeDeleteModal.spec.tsx index dbaae76504..34b743973f 100644 --- a/src/screens/FundCampaignPledge/PledgeDeleteModal.test.tsx +++ b/src/screens/FundCampaignPledge/PledgeDeleteModal.spec.tsx @@ -15,11 +15,12 @@ import i18nForTest from '../../utils/i18nForTest'; import { MOCKS_DELETE_PLEDGE_ERROR, MOCKS } from './PledgesMocks'; import { StaticMockLink } from 'utils/StaticMockLink'; import { toast } from 'react-toastify'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -31,7 +32,7 @@ const translations = JSON.parse( const pledgeProps: InterfaceDeletePledgeModal = { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), pledge: { _id: '1', amount: 100, @@ -47,7 +48,7 @@ const pledgeProps: InterfaceDeletePledgeModal = { }, ], }, - refetchPledge: jest.fn(), + refetchPledge: vi.fn(), }; const renderPledgeDeleteModal = ( From eceaad948a7a85630ee75ca99d36ad7a7d7892f8 Mon Sep 17 00:00:00 2001 From: khushi santosh patil <143253539+khushipatil1523@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:37:13 +0530 Subject: [PATCH 24/32] fix: Update regex to detect all variants of /* istanbul ignore */ comments #3217 (#3221) * fix: Update regex to detect all variants of /* istanbul ignore */ comments * Update code_coverage_disable_check.py * Update code_coverage_disable_check.py * fix: Update regex to detect all variants of /* istanbul ignore */ comments * fix: Update regex to detect all variants of /* istanbul ignore */ comments * fix: Update regex to detect all variants of /* istanbul ignore */ comments --- .github/workflows/scripts/code_coverage_disable_check.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scripts/code_coverage_disable_check.py b/.github/workflows/scripts/code_coverage_disable_check.py index 1a55c3f720..bda4066f03 100644 --- a/.github/workflows/scripts/code_coverage_disable_check.py +++ b/.github/workflows/scripts/code_coverage_disable_check.py @@ -38,10 +38,11 @@ def has_code_coverage_disable(file_path): otherwise. """ code_coverage_disable_pattern = re.compile( - r"""//?\s*istanbul\s+ignore(?:\s+(?:next|-line))?[^\n]*| - /\*\s*istanbul\s+ignore\s+(?:next|-line)\s*\*/""", + r"/\*\s*istanbul\s+ignore.*?\*/|//?\s*istanbul\s+ignore(?:\s+(?:next|-line))?[^\n]*", re.IGNORECASE, ) + + try: with open(file_path, "r", encoding="utf-8") as file: content = file.read() From 9e67f351ad94d1ce714ad7b04ccb93a7966089d1 Mon Sep 17 00:00:00 2001 From: Shiva <148421597+shivasankaran18@users.noreply.github.com> Date: Fri, 10 Jan 2025 22:49:02 +0530 Subject: [PATCH 25/32] refactor:vitest to GroupModal.tsx (#3226) * refactor:vitest to GroupModal.tsx * add ts-doc --- ...roupModal.test.tsx => GroupModal.spec.tsx} | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) rename src/screens/UserPortal/Volunteer/Groups/{GroupModal.test.tsx => GroupModal.spec.tsx} (96%) diff --git a/src/screens/UserPortal/Volunteer/Groups/GroupModal.test.tsx b/src/screens/UserPortal/Volunteer/Groups/GroupModal.spec.tsx similarity index 96% rename from src/screens/UserPortal/Volunteer/Groups/GroupModal.test.tsx rename to src/screens/UserPortal/Volunteer/Groups/GroupModal.spec.tsx index 1d83d9a872..2bfed36882 100644 --- a/src/screens/UserPortal/Volunteer/Groups/GroupModal.test.tsx +++ b/src/screens/UserPortal/Volunteer/Groups/GroupModal.spec.tsx @@ -16,16 +16,22 @@ import { toast } from 'react-toastify'; import type { InterfaceGroupModal } from './GroupModal'; import GroupModal from './GroupModal'; import userEvent from '@testing-library/user-event'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); const link1 = new StaticMockLink(MOCKS); const link2 = new StaticMockLink(UPDATE_ERROR_MOCKS); + +/** + * Translations for test cases + */ + const t = { ...JSON.parse( JSON.stringify( @@ -36,12 +42,16 @@ const t = { ...JSON.parse(JSON.stringify(i18n.getDataByLanguage('en')?.errors ?? {})), }; +/** + * Props for `GroupModal` component used in tests + */ + const itemProps: InterfaceGroupModal[] = [ { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), eventId: 'eventId', - refetchGroups: jest.fn(), + refetchGroups: vi.fn(), group: { _id: 'groupId', name: 'Group 1', @@ -79,9 +89,9 @@ const itemProps: InterfaceGroupModal[] = [ }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), eventId: 'eventId', - refetchGroups: jest.fn(), + refetchGroups: vi.fn(), group: { _id: 'groupId', name: 'Group 1', From 9aa2d1cbcd78f086c9e9e547f1c60532e8c0e510 Mon Sep 17 00:00:00 2001 From: Pranav Nathe <93403830+pranavnathe@users.noreply.github.com> Date: Fri, 10 Jan 2025 22:50:29 +0530 Subject: [PATCH 26/32] Refactor CSS in OrganizationActionItems.tsx (#3242) --- .../OrganizationActionItems/OrganizationActionItems.tsx | 4 ++-- src/style/app.module.css | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/screens/OrganizationActionItems/OrganizationActionItems.tsx b/src/screens/OrganizationActionItems/OrganizationActionItems.tsx index e3d55648b0..26a8c77bc3 100644 --- a/src/screens/OrganizationActionItems/OrganizationActionItems.tsx +++ b/src/screens/OrganizationActionItems/OrganizationActionItems.tsx @@ -186,7 +186,7 @@ function organizationActionItems(): JSX.Element { className={styles.TableImage} /> ) : ( -
+
-
+
Date: Sat, 11 Jan 2025 01:33:43 +0530 Subject: [PATCH 27/32] BugFix : Display user name instead of "undefined undefined" when making a post in an organization (#3241) * Fixed name not rendering issue by adressing the user query properly * Added test case for the name rendering * removed the unnecessary file --- .../UserPortal/StartPostModal/StartPostModal.spec.tsx | 8 ++++++++ src/screens/UserPortal/Posts/Posts.tsx | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/UserPortal/StartPostModal/StartPostModal.spec.tsx b/src/components/UserPortal/StartPostModal/StartPostModal.spec.tsx index a220a166e1..fb33e3c1eb 100644 --- a/src/components/UserPortal/StartPostModal/StartPostModal.spec.tsx +++ b/src/components/UserPortal/StartPostModal/StartPostModal.spec.tsx @@ -152,6 +152,14 @@ describe('Testing StartPostModal Component: User Portal', () => { ); }); + it('should display correct username', async () => { + renderStartPostModal(true, null); + await wait(); + + const userFullName = screen.getByText('Glen dsza'); + expect(userFullName).toBeInTheDocument(); + }); + it('If user image is null then default image should be shown', async () => { renderStartPostModal(true, null); await wait(); diff --git a/src/screens/UserPortal/Posts/Posts.tsx b/src/screens/UserPortal/Posts/Posts.tsx index ceb10c9b49..1c03e64d70 100644 --- a/src/screens/UserPortal/Posts/Posts.tsx +++ b/src/screens/UserPortal/Posts/Posts.tsx @@ -23,6 +23,7 @@ import useLocalStorage from 'utils/useLocalstorage'; import styles from './Posts.module.css'; import convertToBase64 from 'utils/convertToBase64'; import Carousel from 'react-multi-carousel'; +import { TAGS_QUERY_DATA_CHUNK_SIZE } from 'utils/organizationTagsUtils'; import 'react-multi-carousel/lib/styles.css'; const responsive = { @@ -156,7 +157,10 @@ export default function home(): JSX.Element { const userId: string | null = getItem('userId'); const { data: userData } = useQuery(USER_DETAILS, { - variables: { id: userId }, + variables: { + id: userId, + first: TAGS_QUERY_DATA_CHUNK_SIZE, // This is for tagsAssignedWith pagination + }, }); const user: InterfaceQueryUserListItem | undefined = userData?.user; From ea2f4eacded022210b5ad8abf757226c6154107e Mon Sep 17 00:00:00 2001 From: NISHANT SINGH <151461374+NishantSinghhhhh@users.noreply.github.com> Date: Sat, 11 Jan 2025 01:39:54 +0530 Subject: [PATCH 28/32] setup of vitest (#3238) Signed-off-by: NishantSinghhhhh --- vitest.setup.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/vitest.setup.ts b/vitest.setup.ts index 7b0828bfa8..bc1f5663d0 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -1 +1,28 @@ import '@testing-library/jest-dom'; +import { cleanup } from '@testing-library/react'; + +// Basic cleanup after each test +afterEach(() => { + cleanup(); +}); + +// Simple console error handler for React 18 warnings +const originalError = console.error; +beforeAll(() => { + console.error = (...args: unknown[]) => { + const firstArg = args[0]; + if ( + typeof firstArg === 'string' && + /Warning: ReactDOM.render is no longer supported in React 18./.test( + firstArg, + ) + ) { + return; + } + originalError.call(console, ...args); + }; +}); + +afterAll(() => { + console.error = originalError; +}); From d633ebde652d632dca065c8f9b28ded96c53cff8 Mon Sep 17 00:00:00 2001 From: khushi santosh patil <143253539+khushipatil1523@users.noreply.github.com> Date: Sat, 11 Jan 2025 01:47:44 +0530 Subject: [PATCH 29/32] Refactor Pladges.test.txt from jest to vitest #2577 (#3245) * all test are passing * Refactored Pledges tests from Jest to Vitest * Refactored Pledges tests from Jest to Vitest --- .../{Pledge.test.tsx => Pledge.spec.tsx} | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) rename src/screens/UserPortal/Pledges/{Pledge.test.tsx => Pledge.spec.tsx} (90%) diff --git a/src/screens/UserPortal/Pledges/Pledge.test.tsx b/src/screens/UserPortal/Pledges/Pledge.spec.tsx similarity index 90% rename from src/screens/UserPortal/Pledges/Pledge.test.tsx rename to src/screens/UserPortal/Pledges/Pledge.spec.tsx index 3d5eef94c2..a41d95687b 100644 --- a/src/screens/UserPortal/Pledges/Pledge.test.tsx +++ b/src/screens/UserPortal/Pledges/Pledge.spec.tsx @@ -21,20 +21,31 @@ import { EMPTY_MOCKS, MOCKS, USER_PLEDGES_ERROR } from './PledgesMocks'; import type { ApolloLink } from '@apollo/client'; import Pledges from './Pledges'; import useLocalStorage from 'utils/useLocalstorage'; +import { vi, expect, describe, it } from 'vitest'; -jest.mock('react-toastify', () => ({ +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: () => ({ orgId: 'orgId' }), + }; +}); + +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); -jest.mock('@mui/x-date-pickers/DateTimePicker', () => { +vi.mock('@mui/x-date-pickers/DateTimePicker', async () => { + const actualModule = await vi.importActual( + '@mui/x-date-pickers/DesktopDateTimePicker', + ); return { - DateTimePicker: jest.requireActual( - '@mui/x-date-pickers/DesktopDateTimePicker', - ).DesktopDateTimePicker, + DateTimePicker: actualModule.DesktopDateTimePicker, }; }); + const { setItem } = useLocalStorage(); const link1 = new StaticMockLink(MOCKS); @@ -71,15 +82,8 @@ describe('Testing User Pledge Screen', () => { setItem('userId', 'userId'); }); - beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: 'orgId' }), - })); - }); - afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); afterEach(() => { @@ -95,32 +99,35 @@ describe('Testing User Pledge Screen', () => { }); }); + // This test works: it('should redirect to fallback URL if userId is null in LocalStorage', async () => { setItem('userId', null); + renderMyPledges(link1); await waitFor(() => { expect(screen.getByTestId('paramsError')).toBeInTheDocument(); }); }); + // So let's structure our failing test similarly: it('should redirect to fallback URL if URL params are undefined', async () => { render( - + - - - } /> -
} - /> - - + + + + } /> + } /> + + + , ); + await waitFor(() => { expect(screen.getByTestId('paramsError')).toBeInTheDocument(); }); From 56415cfd90994a53a263241f3153839690ff4779 Mon Sep 17 00:00:00 2001 From: Gurram Karthik <167804249+gurramkarthiknetha@users.noreply.github.com> Date: Sat, 11 Jan 2025 04:23:39 +0530 Subject: [PATCH 30/32] fixed:#2502 Refactored CSS files in src/screens/BlockUser (#3215) * initial commit * Update UpdateSession.spec.tsx --- src/screens/BlockUser/BlockUser.module.css | 102 --------------------- src/style/app.module.css | 75 ++++++++++++++- 2 files changed, 72 insertions(+), 105 deletions(-) delete mode 100644 src/screens/BlockUser/BlockUser.module.css diff --git a/src/screens/BlockUser/BlockUser.module.css b/src/screens/BlockUser/BlockUser.module.css deleted file mode 100644 index ed93446206..0000000000 --- a/src/screens/BlockUser/BlockUser.module.css +++ /dev/null @@ -1,102 +0,0 @@ -.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 .inputContainer { - flex: 1; - position: relative; -} - -.btnsContainer .input { - width: 70%; - position: relative; -} - -.btnsContainer input { - outline: 1px solid var(--bs-gray-400); -} - -.btnsContainer .inputContainer button { - width: 52px; -} - -.largeBtnsWrapper { - display: flex; -} - -.listBox { - width: 100%; - flex: 1; -} - -.notFound { - flex: 1; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -} - -@media (max-width: 1020px) { - .btnsContainer { - flex-direction: column; - margin: 1.5rem 0; - } - - .btnsContainer .input { - width: 100%; - } - - .btnsContainer .btnsBlock { - margin: 1.5rem 0 0 0; - justify-content: space-between; - } - - .btnsContainer .btnsBlock button { - margin: 0; - } - - .btnsContainer .btnsBlock div button { - margin-right: 1.5rem; - } -} - -/* For mobile devices */ - -@media (max-width: 520px) { - .btnsContainer { - margin-bottom: 0; - } - - .btnsContainer .btnsBlock { - display: block; - margin-top: 1rem; - margin-right: 0; - } - - .largeBtnsWrapper { - flex-direction: column; - } - - .btnsContainer .btnsBlock div { - flex: 1; - } - - .btnsContainer .btnsBlock button { - margin-bottom: 1rem; - margin-right: 0; - width: 100%; - } -} diff --git a/src/style/app.module.css b/src/style/app.module.css index b8746abf1a..6763be4b24 100644 --- a/src/style/app.module.css +++ b/src/style/app.module.css @@ -706,7 +706,7 @@ hr { top: 50%; width: 50%; height: 1px; - background: #fff; + background: var(--white); content: ''; } .pageNotFound h1.head span:before { @@ -3746,7 +3746,7 @@ button[data-testid='createPostBtn'] { .primaryText { font-weight: bold; - color: var(--bs-emphasis-color, #000); + color: var(--bs-emphasis-color, var(--black-color)); @extend .reusable-text-ellipsis; /* Referencing the reusable class from the general section */ } @@ -3799,7 +3799,7 @@ button[data-testid='createPostBtn'] { .inactiveButton { background-color: transparent; - color: var(--bs-emphasis-color, #000); + color: var(--bs-emphasis-color, var(--black-color)); &:hover { background-color: var(--grey-bg-color); @@ -5970,3 +5970,72 @@ button[data-testid='createPostBtn'] { margin-left: 13vw; max-width: 80vw; } + +.btnsContainer .input { + width: 70%; +} + +.btnsContainer .inputContainer button { + width: 52px; +} + +.largeBtnsWrapper { + display: flex; +} + +.listBox { + width: 100%; + flex: 1; +} + +@media (max-width: 1020px) { + .btnsContainer { + flex-direction: column; + margin: 1.5rem 0; + } + + .btnsContainer .input { + width: 100%; + } + + .btnsContainer .btnsBlock { + margin: 1.5rem 0 0 0; + justify-content: space-between; + } + + .btnsContainer .btnsBlock button { + margin: 0; + } + + .btnsContainer .btnsBlock div button { + margin-right: 1.5rem; + } +} + +/* For mobile devices */ + +@media (max-width: 520px) { + .btnsContainer { + margin-bottom: 0; + } + + .btnsContainer .btnsBlock { + display: block; + margin-top: 1rem; + margin-right: 0; + } + + .largeBtnsWrapper { + flex-direction: column; + } + + .btnsContainer .btnsBlock div { + flex: 1; + } + + .btnsContainer .btnsBlock button { + margin-bottom: 1rem; + margin-right: 0; + width: 100%; + } +} From e343b0c2720f37aaac0fb36bfb1e60fd6282b8b0 Mon Sep 17 00:00:00 2001 From: Aadhil Ahamed Date: Sat, 11 Jan 2025 09:22:54 +0530 Subject: [PATCH 31/32] Fixed LeftDrawerOrg Visibility (#3239) * fixed LeftDrawerOrg Visibility * Added tests for Visiblity in both LeftDrawer and LeftDrawerOrg --- src/components/LeftDrawer/LeftDrawer.spec.tsx | 36 ++++++++++++++++ .../LeftDrawerOrg/LeftDrawerOrg.spec.tsx | 43 +++++++++++++++++++ .../LeftDrawerOrg/LeftDrawerOrg.tsx | 7 +++ 3 files changed, 86 insertions(+) diff --git a/src/components/LeftDrawer/LeftDrawer.spec.tsx b/src/components/LeftDrawer/LeftDrawer.spec.tsx index dc22717e3d..a0aaf9336c 100644 --- a/src/components/LeftDrawer/LeftDrawer.spec.tsx +++ b/src/components/LeftDrawer/LeftDrawer.spec.tsx @@ -220,4 +220,40 @@ describe('Testing Left Drawer component for ADMIN', () => { expect(global.window.location.pathname).toContain('/orglist'); }); + + it('Should set hideDrawer to false when initially null', async () => { + const mockSetHideDrawer = vi.fn(); + await act(async () => { + render( + + + + + + + , + ); + }); + expect(mockSetHideDrawer).toHaveBeenCalledWith(false); + expect(mockSetHideDrawer).toHaveBeenCalledTimes(1); + }); + + it('Should not call setHideDrawer when hideDrawer has a value', async () => { + const mockSetHideDrawer = vi.fn(); + await act(async () => { + render( + + + + + + + , + ); + }); + expect(mockSetHideDrawer).not.toHaveBeenCalled(); + }); }); diff --git a/src/components/LeftDrawerOrg/LeftDrawerOrg.spec.tsx b/src/components/LeftDrawerOrg/LeftDrawerOrg.spec.tsx index 3fa6c0205e..f0e5d446d1 100644 --- a/src/components/LeftDrawerOrg/LeftDrawerOrg.spec.tsx +++ b/src/components/LeftDrawerOrg/LeftDrawerOrg.spec.tsx @@ -472,4 +472,47 @@ describe('Testing LeftDrawerOrg component for SUPERADMIN', () => { , ); }); + + test('Should set hideDrawer to false when initially null', async () => { + const mockSetHideDrawer = vi.fn(); + render( + + + + + + + + + , + ); + await wait(); + expect(mockSetHideDrawer).toHaveBeenCalledWith(false); + expect(mockSetHideDrawer).toHaveBeenCalledTimes(1); + }); + + test('Should not call setHideDrawer when hideDrawer has a value', async () => { + const mockSetHideDrawer = vi.fn(); + render( + + + + + + + + + , + ); + await wait(); + expect(mockSetHideDrawer).not.toHaveBeenCalled(); + }); }); diff --git a/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx b/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx index 35173a930e..de9583843c 100644 --- a/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx +++ b/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx @@ -74,6 +74,13 @@ const leftDrawerOrg = ({ () => getIdFromPath(location.pathname), [location.pathname], ); + + useEffect(() => { + if (hideDrawer === null) { + setHideDrawer(false); + } + }, []); + // Check if the current page is admin profile page useEffect(() => { From e3a2bdfb6794437262d21066f51602b1ff679495 Mon Sep 17 00:00:00 2001 From: Harshil Gupta <144909381+hars-21@users.noreply.github.com> Date: Sat, 11 Jan 2025 20:09:06 +0530 Subject: [PATCH 32/32] Improve code coverage in Pagination.tsx (#3246) --- src/components/Pagination/Pagination.spec.tsx | 137 ++++++++++++------ src/components/Pagination/Pagination.tsx | 3 - 2 files changed, 96 insertions(+), 44 deletions(-) diff --git a/src/components/Pagination/Pagination.spec.tsx b/src/components/Pagination/Pagination.spec.tsx index 873f790565..ed56a83092 100644 --- a/src/components/Pagination/Pagination.spec.tsx +++ b/src/components/Pagination/Pagination.spec.tsx @@ -6,60 +6,115 @@ import { createTheme, ThemeProvider } from '@mui/material/styles'; import Pagination from './Pagination'; import { store } from 'state/store'; import userEvent from '@testing-library/user-event'; -import { describe, it } from 'vitest'; - -describe('Testing Pagination component', () => { - const props = { - count: 5, - page: 10, - rowsPerPage: 5, - onPageChange: (): number => { - return 10; - }, +import { describe, it, vi, expect } from 'vitest'; + +describe('Pagination component tests', () => { + const mockOnPageChange = vi.fn(); + + const defaultProps = { + count: 20, // Total items + page: 2, // Current page + rowsPerPage: 5, // Items per page + onPageChange: mockOnPageChange, // Mocked callback for page change }; - it('Component should be rendered properly on rtl', async () => { + it('should render all pagination buttons and invoke onPageChange for navigation', async () => { render( - + , ); + + // Ensure all buttons are rendered + expect(screen.getByTestId('firstPage')).toBeInTheDocument(); + expect(screen.getByTestId('previousPage')).toBeInTheDocument(); + expect(screen.getByTestId('nextPage')).toBeInTheDocument(); + expect(screen.getByTestId('lastPage')).toBeInTheDocument(); + + // Simulate button clicks and verify callback invocation + await act(async () => { + userEvent.click(screen.getByTestId('nextPage')); + }); + expect(mockOnPageChange).toHaveBeenCalledWith(expect.anything(), 3); // Next page + + await act(async () => { + userEvent.click(screen.getByTestId('previousPage')); + }); + expect(mockOnPageChange).toHaveBeenCalledWith(expect.anything(), 1); // Previous page + + await act(async () => { + userEvent.click(screen.getByTestId('firstPage')); + }); + expect(mockOnPageChange).toHaveBeenCalledWith(expect.anything(), 0); // First page + await act(async () => { - userEvent.click(screen.getByTestId(/nextPage/i)); - userEvent.click(screen.getByTestId(/previousPage/i)); + userEvent.click(screen.getByTestId('lastPage')); }); + expect(mockOnPageChange).toHaveBeenCalledWith(expect.anything(), 3); // Last page }); -}); -const props = { - count: 5, - page: 10, - rowsPerPage: 5, - onPageChange: (): number => { - return 10; - }, - theme: { direction: 'rtl' }, -}; - -it('Component should be rendered properly', async () => { - const theme = createTheme({ - direction: 'rtl', + it('should disable navigation buttons at the boundaries', () => { + render( + + + + + , + ); + + // First and Previous buttons should be disabled on the first page + expect(screen.getByTestId('firstPage')).toBeDisabled(); + expect(screen.getByTestId('previousPage')).toBeDisabled(); + + // Next and Last buttons should not be disabled + expect(screen.getByTestId('nextPage')).not.toBeDisabled(); + expect(screen.getByTestId('lastPage')).not.toBeDisabled(); }); - render( - - - - - - - , - ); - - await act(async () => { - userEvent.click(screen.getByTestId(/nextPage/i)); - userEvent.click(screen.getByTestId(/previousPage/i)); + it('should render correctly with RTL direction', async () => { + const rtlTheme = createTheme({ direction: 'rtl' }); + + render( + + + + + + + , + ); + + // Verify buttons render properly in RTL mode + expect(screen.getByTestId('firstPage')).toBeInTheDocument(); + expect(screen.getByTestId('lastPage')).toBeInTheDocument(); + + // Simulate a button click in RTL mode + await act(async () => { + userEvent.click(screen.getByTestId('nextPage')); + }); + expect(mockOnPageChange).toHaveBeenCalledWith(expect.anything(), 3); // Next page + }); + + it('should disable Next and Last buttons on the last page', () => { + render( + + + + + , + ); + + // Next and Last buttons should be disabled on the last page + expect(screen.getByTestId('nextPage')).toBeDisabled(); + expect(screen.getByTestId('lastPage')).toBeDisabled(); + + // First and Previous buttons should not be disabled + expect(screen.getByTestId('firstPage')).not.toBeDisabled(); + expect(screen.getByTestId('previousPage')).not.toBeDisabled(); }); }); diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx index 637492a289..9ef46bacdc 100644 --- a/src/components/Pagination/Pagination.tsx +++ b/src/components/Pagination/Pagination.tsx @@ -37,7 +37,6 @@ function pagination(props: InterfaceTablePaginationActionsProps): JSX.Element { * * @param event - The click event. */ - /* istanbul ignore next */ const handleFirstPageButtonClick = ( event: React.MouseEvent, ): void => { @@ -63,7 +62,6 @@ function pagination(props: InterfaceTablePaginationActionsProps): JSX.Element { * @param event - The click event. */ - /* istanbul ignore next */ const handleNextButtonClick = ( event: React.MouseEvent, ): void => { @@ -76,7 +74,6 @@ function pagination(props: InterfaceTablePaginationActionsProps): JSX.Element { * * @param event - The click event. */ - /* istanbul ignore next */ const handleLastPageButtonClick = ( event: React.MouseEvent, ): void => {