From 400999dc735d244512443525e93862f34d8654d8 Mon Sep 17 00:00:00 2001 From: Aditya Dubey Date: Sat, 29 Aug 2026 19:12:27 -0700 Subject: [PATCH 1/2] fix: correct project totals on the Projects page Total Projects counted whatever list was currently in the store, and the archive toggle replaced that list rather than adding to it, so the figure changed from 1189 to 458 simply by switching views. It should stay constant at the combined total in both. Archived projects now load into their own key rather than overwriting `projects`, which several other components read. Both lists are fetched on mount so the total is right before the archived view is ever opened, and the second card switches between the active and archived counts with a matching label. - Add FETCH_ARCHIVED_PROJECTS_SUCCESS so the archived fetch stops clobbering `projects` - Hold archived projects under `archivedProjects` in allProjectsReducer - Total is now projects + archivedProjects; the list reads from whichever array matches the current view - Overview takes a label and count for the second card, so the archived view reads "Archived Projects" rather than "Active Projects" - Add ARCHIVED_PROJECTS label - Two reducer tests covering that neither list overwrites the other --- src/actions/projects.js | 13 ++++++- src/components/Projects/Overview/Overview.jsx | 9 +++-- src/components/Projects/Projects.jsx | 31 ++++++++++++---- src/constants/projects.js | 5 +++ src/languages/en/ui.js | 1 + .../__tests__/allProjectsReducer.test.js | 35 +++++++++++++++++++ src/reducers/allProjectsReducer.js | 13 +++++++ 7 files changed, 97 insertions(+), 10 deletions(-) diff --git a/src/actions/projects.js b/src/actions/projects.js index 3bab9412813..99d5aec0683 100644 --- a/src/actions/projects.js +++ b/src/actions/projects.js @@ -33,6 +33,17 @@ const setProjectsError = ({ status, error }) => ({ error, }); +/** + * set archived projects in store, under their own key + * @param projects: archived projects + * @param status: status code + */ +const setArchivedProjectsSuccess = ({ projects, status }) => ({ + type: types.FETCH_ARCHIVED_PROJECTS_SUCCESS, + projects, + status, +}); + /** * Add new project to store * @param payload : new project @@ -115,7 +126,7 @@ export const fetchAllArchivedProjects = () => { const res = await axios.get(url); status = res.status; const projects = res.data; - dispatch(setProjectsSuccess({ projects, status })); + dispatch(setArchivedProjectsSuccess({ projects, status })); } catch (err) { status = err.response.status; error = err.response.data; diff --git a/src/components/Projects/Overview/Overview.jsx b/src/components/Projects/Overview/Overview.jsx index 9fd80d29ced..3a6c747bb2c 100644 --- a/src/components/Projects/Overview/Overview.jsx +++ b/src/components/Projects/Overview/Overview.jsx @@ -4,10 +4,15 @@ * This component display the number of projects and active projects ********************************************************************************/ import React from 'react'; -import { TOTAL_PROJECTS, ACTIVE_PROJECTS } from './../../../languages/en/ui'; +import { TOTAL_PROJECTS, ACTIVE_PROJECTS, ARCHIVED_PROJECTS } from './../../../languages/en/ui'; import styles from "./Overview.module.css" const Overview = props => { + // The second card describes whichever list is on screen: active projects in + // the default view, archived projects once the archived view is opened. + const secondCardLabel = props.showArchived ? ARCHIVED_PROJECTS : ACTIVE_PROJECTS; + const secondCardCount = props.showArchived ? props.numberOfArchived : props.numberOfActive; + return (
@@ -21,7 +26,7 @@ const Overview = props => {
- {ACTIVE_PROJECTS}: {props.numberOfActive} + {secondCardLabel}: {secondCardCount}
diff --git a/src/components/Projects/Projects.jsx b/src/components/Projects/Projects.jsx index bfc9681a0a7..f5eed57935c 100644 --- a/src/components/Projects/Projects.jsx +++ b/src/components/Projects/Projects.jsx @@ -20,6 +20,10 @@ import Loading from '../common/Loading'; import hasPermission from '../../utils/permissions'; import EditableInfoModal from '../UserProfile/EditableModal/EditableInfoModal'; +// Stable reference for the empty case. Returning a fresh [] from a selector +// gives a new identity on every render, which retriggers effects that depend +// on it. +const EMPTY_PROJECT_LIST = []; const Projects = function(props) { const { role } = props.state.userProfile; @@ -28,9 +32,14 @@ const Projects = function(props) { const taskSelectionMode = location.state?.taskSelectionMode || false; const taskSelectionReturnPath = location.state?.returnPath || '/bmdashboard/AddNewTeam'; const allReduxProjects = useSelector(state => state.allProjects.projects); - const numberOfProjects = props.state.allProjects.projects.length; - const numberOfActive = props.state.allProjects.projects.filter(project => project.isActive) - .length; + const archivedReduxProjects = useSelector( + state => state.allProjects.archivedProjects ?? EMPTY_PROJECT_LIST, + ); + // Total counts every project the app knows about, so it does not change when + // the archived view is toggled. The second card switches between the active + // count and the archived count depending on which list is on screen. + const numberOfProjects = allReduxProjects.length + archivedReduxProjects.length; + const numberOfActive = allReduxProjects.filter(project => project.isActive).length; const { fetching, fetched, status, error } = props.state.allProjects; const initialModalData = { showModal: false, @@ -197,8 +206,8 @@ const Projects = function(props) { const generateProjectList = (categorySelectedForSort, showStatus, isShowingArchived) => { const activeMemberCounts = props.state.projectMembers?.activeMemberCounts || {}; - const filteredProjects = allReduxProjects - .filter(project => isShowingArchived ? project.isArchived : !project.isArchived) + const sourceProjects = isShowingArchived ? archivedReduxProjects : allReduxProjects; + const filteredProjects = sourceProjects .filter(project => { if (categorySelectedForSort && showStatus){ return project.category === categorySelectedForSort && project.isActive === showStatus; @@ -272,7 +281,10 @@ const Projects = function(props) { useEffect(() => { + // Both lists are loaded up front so the total is correct before the + // archived view is ever opened. props.fetchAllProjects(); + props.fetchAllArchivedProjects(); }, []); useEffect(() => { @@ -290,7 +302,7 @@ const Projects = function(props) { hasInactiveBtn: false, }); } - }, [categorySelectedForSort, showStatus, sorter, allReduxProjects, props.state.theme.darkMode, props.state.projectMembers?.activeMemberCounts, showArchived]); + }, [categorySelectedForSort, showStatus, sorter, allReduxProjects, archivedReduxProjects, props.state.theme.darkMode, props.state.projectMembers?.activeMemberCounts, showArchived]); useEffect(() => { const fetchProjects = async () => { @@ -369,7 +381,12 @@ const Projects = function(props) { isPermissionPage={true} role={role} /> - + {canPostProject ? : null} {taskSelectionMode && (
diff --git a/src/constants/projects.js b/src/constants/projects.js index 3abee2ebed6..30560f27f74 100644 --- a/src/constants/projects.js +++ b/src/constants/projects.js @@ -8,6 +8,11 @@ export const FETCH_PROJECTS_START = 'FETCH_PROJECTS_START'; export const FETCH_PROJECTS_SUCCESS = 'FETCH_PROJECTS_SUCCESS'; export const FETCH_PROJECTS_ERROR = 'FETCH_PROJECTS_ERROR'; +// ARCHIVED PROJECTS +// Kept separate from FETCH_PROJECTS_SUCCESS so that loading the archived list +// does not overwrite `projects`, which several other components read. +export const FETCH_ARCHIVED_PROJECTS_SUCCESS = 'FETCH_ARCHIVED_PROJECTS_SUCCESS'; + // ADD NEW PROJECTS export const ADD_NEW_PROJECT = 'ADD_NEW_PROJECT'; export const ADD_NEW_PROJECT_ERROR = 'ADD_NEW_PROJECT_ERROR'; diff --git a/src/languages/en/ui.js b/src/languages/en/ui.js index a7293c20ea1..6fe7b523a0f 100644 --- a/src/languages/en/ui.js +++ b/src/languages/en/ui.js @@ -6,6 +6,7 @@ export const ACTIVE = 'Active'; export const TITLE = 'Title' export const INACTIVE = 'InActive'; export const ACTIVE_PROJECTS = 'Active Projects'; +export const ARCHIVED_PROJECTS = 'Archived Projects'; export const BM_DASHBOARD = 'BM Dashboard'; export const CP_DASHBOARD = 'CP Dashboard'; export const BM_PROJECT = 'Project'; diff --git a/src/reducers/__tests__/allProjectsReducer.test.js b/src/reducers/__tests__/allProjectsReducer.test.js index 6f51293d266..5dffae74026 100644 --- a/src/reducers/__tests__/allProjectsReducer.test.js +++ b/src/reducers/__tests__/allProjectsReducer.test.js @@ -6,6 +6,7 @@ describe('allProjectsReducer', () => { fetching: false, fetched: false, projects: [], + archivedProjects: [], status: 200, error: null, }; @@ -59,6 +60,40 @@ describe('allProjectsReducer', () => { expect(newState).toEqual(expectedState); }); + it('should handle FETCH_ARCHIVED_PROJECTS_SUCCESS without touching projects', () => { + const stateWithProjects = { + ...initialState, + projects: [{ _id: '1', isActive: true }], + }; + const archived = [{ _id: '2' }, { _id: '3' }]; + const action = { + type: types.FETCH_ARCHIVED_PROJECTS_SUCCESS, + projects: archived, + status: 200, + }; + const newState = allProjectsReducer(stateWithProjects, action); + + expect(newState.archivedProjects).toEqual(archived); + // The archived list must not overwrite `projects` — several other + // components read that key. + expect(newState.projects).toEqual(stateWithProjects.projects); + expect(newState.fetched).toBe(true); + expect(newState.fetching).toBe(false); + }); + + it('should keep archivedProjects when FETCH_PROJECTS_SUCCESS arrives', () => { + const stateWithArchived = { ...initialState, archivedProjects: [{ _id: '9' }] }; + const action = { + type: types.FETCH_PROJECTS_SUCCESS, + projects: [{ _id: '1' }], + status: 200, + }; + const newState = allProjectsReducer(stateWithArchived, action); + + expect(newState.projects).toEqual([{ _id: '1' }]); + expect(newState.archivedProjects).toEqual([{ _id: '9' }]); + }); + it('should handle ADD_NEW_PROJECT with successful status', () => { const newProject = { _id: 3, name: 'Project 3' }; const action = { diff --git a/src/reducers/allProjectsReducer.js b/src/reducers/allProjectsReducer.js index 083969692bb..fec5c133d50 100644 --- a/src/reducers/allProjectsReducer.js +++ b/src/reducers/allProjectsReducer.js @@ -4,6 +4,10 @@ const allProjectsInital = { fetching: false, fetched: false, projects: [], + // Archived projects are held separately so that `projects` always means + // "not archived". The Projects page needs both counts at once to show a + // total that does not change when the archived view is toggled. + archivedProjects: [], status: 200, error: null, }; @@ -35,6 +39,15 @@ export const allProjectsReducer = (allProjects = allProjectsInital, action) => { }); } + case types.FETCH_ARCHIVED_PROJECTS_SUCCESS: { + return updateState({ + fetching: false, + fetched: true, + archivedProjects: action.projects, + status, + }); + } + case types.ADD_NEW_PROJECT: { if (status !== 201) return updateState({ status, error }); const { newProject } = action; From bf8f34c56c3e920788efc1769a813ad249807b0f Mon Sep 17 00:00:00 2001 From: Aditya Dubey Date: Sat, 12 Sep 2026 19:44:47 -0700 Subject: [PATCH 2/2] fix: preserve archived project search and dark toggle --- src/components/Projects/Projects.jsx | 24 +++++-- .../Projects/__tests__/Projects.test.jsx | 64 ++++++++++++++++++- src/components/Projects/projects.module.css | 15 ++++- 3 files changed, 97 insertions(+), 6 deletions(-) diff --git a/src/components/Projects/Projects.jsx b/src/components/Projects/Projects.jsx index f5eed57935c..591ad27a515 100644 --- a/src/components/Projects/Projects.jsx +++ b/src/components/Projects/Projects.jsx @@ -311,11 +311,16 @@ const Projects = function(props) { return; } + // Search the same collection that is currently rendered. Archived projects + // live in their own reducer key, so searching allReduxProjects here would + // incorrectly return active projects while the archived view is open. + const visibleProjects = showArchived ? archivedReduxProjects : allReduxProjects; + // Mode 1: Search by user if (searchMode === 'person') { const userProjects = await props.getProjectsByUsersName(debouncedSearchName); - const filteredProjects = allReduxProjects.filter(p => + const filteredProjects = visibleProjects.filter(p => userProjects.includes(p._id) ); @@ -335,7 +340,7 @@ const Projects = function(props) { setProjectList(mapped); } else if (searchMode === 'project') { - const filteredProjects = allReduxProjects.filter(p => + const filteredProjects = visibleProjects.filter(p => p.projectName?.toLowerCase().includes(debouncedSearchName.toLowerCase()) ); @@ -358,7 +363,14 @@ const Projects = function(props) { }; fetchProjects(); -}, [debouncedSearchName, searchMode, allProjects, allReduxProjects]); +}, [ + debouncedSearchName, + searchMode, + allProjects, + allReduxProjects, + archivedReduxProjects, + showArchived, +]); const handleSearchName = searchNameInput => { setSearchName(searchNameInput); @@ -424,7 +436,11 @@ const Projects = function(props) { onClick={handleFetchArchivedProjects} style={{ whiteSpace: 'nowrap', height: '38px', flexShrink: 0 }} className={`btn px-3 ${ - showArchived ? 'btn-warning' : darkMode ? 'btn-outline-light' : 'btn-outline-secondary' + darkMode + ? styles.archiveToggleDark + : showArchived + ? 'btn-warning' + : 'btn-outline-secondary' }`} > {showArchived ? 'Hide Archived' : 'Show Archived'} diff --git a/src/components/Projects/__tests__/Projects.test.jsx b/src/components/Projects/__tests__/Projects.test.jsx index 18260b8bab3..5ea97dc9994 100644 --- a/src/components/Projects/__tests__/Projects.test.jsx +++ b/src/components/Projects/__tests__/Projects.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen, fireEvent} from '@testing-library/react'; +import { act, render, screen, fireEvent} from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import Projects from '..'; import { Provider } from 'react-redux'; @@ -214,5 +214,67 @@ describe("Projects component",()=>{ // fireEvent.click(closeButton) // expect(screen.queryByText('Confirm Archive')).not.toBeInTheDocument(); }) + + it('searches archived projects while the archived view is open', async () => { + axios.get.mockResolvedValue({ status: 200, data: [] }); + const activeProject = { ...projects[0], _id: 'active-project', projectName: 'Active Alpha' }; + const archivedProject = { + ...projects[0], + _id: 'archived-project', + projectName: 'Archived Alpha', + isArchived: true, + }; + const archivedStore = mockStore({ + ...store.getState(), + allProjects: { + projects: [activeProject], + archivedProjects: [archivedProject], + status: 200, + fetching: false, + fetched: true, + }, + }); + + render( + + + + + , + ); + + fireEvent.click(screen.getByRole('button', { name: 'Show Archived' })); + fireEvent.change(screen.getByLabelText('Filter by'), { target: { value: 'project' } }); + fireEvent.change(screen.getByPlaceholderText('Search by Project Name'), { + target: { value: 'Archived Alpha' }, + }); + + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 350)); + }); + + expect(screen.getByText('Archived Alpha')).toBeInTheDocument(); + expect(screen.queryByText('Active Alpha')).not.toBeInTheDocument(); + }); + + it('does not use the light Bootstrap button treatment in dark mode', () => { + axios.get.mockResolvedValue({ status: 200, data: [] }); + const darkStore = mockStore({ + ...store.getState(), + theme: { darkMode: true }, + }); + + render( + + + + + , + ); + + expect(screen.getByRole('button', { name: 'Show Archived' })).not.toHaveClass( + 'btn-outline-light', + ); + }); }) diff --git a/src/components/Projects/projects.module.css b/src/components/Projects/projects.module.css index dfe8a6aad46..0e0455def81 100644 --- a/src/components/Projects/projects.module.css +++ b/src/components/Projects/projects.module.css @@ -3,6 +3,19 @@ background: aliceblue; } +.archiveToggleDark { + color: #f8f9fa; + background-color: #2f415a; + border-color: #8fa3b8; +} + +.archiveToggleDark:hover, +.archiveToggleDark:focus { + color: #fff; + background-color: #405675; + border-color: #b8c7d6; +} + :global(#new_project) { margin-bottom: 10px; } @@ -229,4 +242,4 @@ tr:hover { } .searchLabelDark { background-color: #1f3b63 !important; -} \ No newline at end of file +}