Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/actions/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 7 additions & 2 deletions src/components/Projects/Overview/Overview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="projects__overview--top">
<div className={`${styles["card_project"]} m-2`} id="card_project">
Expand All @@ -21,7 +26,7 @@ const Overview = props => {
<div className={`${styles["card_active"]} m-2`} id="card_active">
<div className={`${styles["card-body"]} card-body`}>
<h6 className={`${styles["card-text"]} card-text ml-3`}>
<i className="fa fa-circle fa-circle-isActive" aria-hidden="true"></i> {ACTIVE_PROJECTS}: {props.numberOfActive}
<i className="fa fa-circle fa-circle-isActive" aria-hidden="true"></i> {secondCardLabel}: {secondCardCount}
</h6>
</div>
</div>
Expand Down
55 changes: 44 additions & 11 deletions src/components/Projects/Projects.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
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;
Expand All @@ -28,9 +32,14 @@
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,
Expand Down Expand Up @@ -197,8 +206,8 @@

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;
Expand Down Expand Up @@ -272,7 +281,10 @@


useEffect(() => {
// Both lists are loaded up front so the total is correct before the
// archived view is ever opened.
props.fetchAllProjects();
props.fetchAllArchivedProjects();
}, []);

useEffect(() => {
Expand All @@ -290,7 +302,7 @@
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 () => {
Expand All @@ -299,11 +311,16 @@
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)
);

Expand All @@ -323,7 +340,7 @@

setProjectList(mapped);
} else if (searchMode === 'project') {
const filteredProjects = allReduxProjects.filter(p =>
const filteredProjects = visibleProjects.filter(p =>
p.projectName?.toLowerCase().includes(debouncedSearchName.toLowerCase())
);

Expand All @@ -346,7 +363,14 @@
};

fetchProjects();
}, [debouncedSearchName, searchMode, allProjects, allReduxProjects]);
}, [
debouncedSearchName,
searchMode,
allProjects,
allReduxProjects,
archivedReduxProjects,
showArchived,
]);

const handleSearchName = searchNameInput => {
setSearchName(searchNameInput);
Expand All @@ -369,7 +393,12 @@
isPermissionPage={true}
role={role}
/>
<Overview numberOfProjects={numberOfProjects} numberOfActive={numberOfActive} />
<Overview
numberOfProjects={numberOfProjects}
numberOfActive={numberOfActive}
numberOfArchived={archivedReduxProjects.length}
showArchived={showArchived}
/>
{canPostProject ? <AddProject hasPermission={hasPermission} /> : null}
{taskSelectionMode && (
<div className="alert alert-info mb-2" role="alert">
Expand Down Expand Up @@ -407,7 +436,11 @@
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'

Check warning on line 443 in src/components/Projects/Projects.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AaCYqKzhNo70Q9qDSyi4&open=AaCYqKzhNo70Q9qDSyi4&pullRequest=5484
}`}
>
{showArchived ? 'Hide Archived' : 'Show Archived'}
Expand Down
64 changes: 63 additions & 1 deletion src/components/Projects/__tests__/Projects.test.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(
<MemoryRouter>
<Provider store={archivedStore}>
<Projects />
</Provider>
</MemoryRouter>,
);

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(
<MemoryRouter>
<Provider store={darkStore}>
<Projects />
</Provider>
</MemoryRouter>,
);

expect(screen.getByRole('button', { name: 'Show Archived' })).not.toHaveClass(
'btn-outline-light',
);
});

})
15 changes: 14 additions & 1 deletion src/components/Projects/projects.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -229,4 +242,4 @@ tr:hover {
}
.searchLabelDark {
background-color: #1f3b63 !important;
}
}
5 changes: 5 additions & 0 deletions src/constants/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions src/languages/en/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
35 changes: 35 additions & 0 deletions src/reducers/__tests__/allProjectsReducer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe('allProjectsReducer', () => {
fetching: false,
fetched: false,
projects: [],
archivedProjects: [],
status: 200,
error: null,
};
Expand Down Expand Up @@ -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 = {
Expand Down
13 changes: 13 additions & 0 deletions src/reducers/allProjectsReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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;
Expand Down
Loading