Skip to content
Merged
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
4 changes: 2 additions & 2 deletions frontend/src/components/projects/clearFilters.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { FormattedMessage } from 'react-intl';

import messages from './messages';

export default function ClearFilters({ url, className = '' }: Object) {
export default function ClearFilters({ url, className = '', onClick }: Object) {
return (
<Link to={url} className={`red link ph3 pv2 f6 ${className}`}>
<Link to={url} className={`red link ph3 pv2 f6 ${className}`} onClick={onClick}>
<FormattedMessage {...messages.clearFilters} />
</Link>
);
Expand Down
59 changes: 12 additions & 47 deletions frontend/src/components/projects/exploreProjectsTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { Link } from 'react-router-dom';
import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table';
import { FormattedMessage } from 'react-intl';
import { formatDistance } from 'date-fns';
import { Tooltip } from 'react-tooltip';
import PropTypes from 'prop-types';

import { ProgressBar } from '../progressBar';
import { PriorityBox } from '../projectCard/priorityBox';
import messages from './messages';
import { CircleExclamationIcon } from '../svgIcons';
Expand Down Expand Up @@ -51,55 +49,22 @@ const COLUMNS = [
),
},
{
accessorKey: 'progress',
accessorKey: 'percentMapped',
minSize: 130,
header: () => (
<span>
<FormattedMessage {...messages.progressColumn} />
<FormattedMessage {...messages.percentMapped} />
</span>
),
},
{
accessorKey: 'percentValidated',
minSize: 137,
header: () => (
<span>
<FormattedMessage {...messages.percentValidated} />
</span>
),
minSize: 130,
cell: ({ row }) => {
const tooltipContent = (
<div>
<p className="lh-copy ma0 white f7 fw4">
<FormattedMessage
{...messages['percentMapped']}
values={{ n: <span className="fw8">{row.original.percentMapped}</span> }}
/>
</p>
<p className="lh-copy ma0 white f7 fw4">
<FormattedMessage
{...messages['percentValidated']}
values={{ n: <span className="fw8">{row.original.percentValidated}</span> }}
/>
</p>
</div>
);

return (
<div>
<div data-tooltip-id={`project-${row.original.projectId}-progress`}>
<ProgressBar
firstBarValue={
row.original.percentMapped > 0
? Math.max(8, row.original.percentMapped)
: row.original.percentMapped
}
secondBarValue={
row.original.percentValidated > 0
? Math.max(8, row.original.percentValidated)
: row.original.percentValidated
}
height="half"
small={false}
/>
</div>
<Tooltip id={`project-${row.original.projectId}-progress`} className="z-3">
{tooltipContent}
</Tooltip>
</div>
);
},
},
{
accessorKey: 'totalContributors',
Expand Down
10 changes: 7 additions & 3 deletions frontend/src/components/projects/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export default defineMessages({
},
allprojects: {
id: 'project.nav.allprojects',
defaultMessage: 'All',
defaultMessage: 'All Projects',
},
active: {
id: 'project.nav.active',
Expand All @@ -192,6 +192,10 @@ export default defineMessages({
id: 'project.nav.archived',
defaultMessage: 'Archived',
},
published: {
id: 'project.nav.published',
defaultMessage: 'Published',
},
paginationCount: {
id: 'project.pagination.count',
defaultMessage: 'Showing {number} of {total} projects',
Expand Down Expand Up @@ -314,11 +318,11 @@ export default defineMessages({
},
percentMapped: {
id: 'project.table.percentMapped',
defaultMessage: '{n}% mapped',
defaultMessage: 'Percent mapped',
},
percentValidated: {
id: 'project.table.percentValidated',
defaultMessage: '{n}% validated',
defaultMessage: 'Percent validated',
},
downloadAsCSV: {
id: 'project.table.downloadAsCSV',
Expand Down
24 changes: 23 additions & 1 deletion frontend/src/components/projects/projectNav.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ProjectSearchBox } from './projectSearchBox';
import ClearFilters from './clearFilters';
import { OrderBySelector } from './orderBy';
import { ProjectsActionFilter } from './projectsActionFilter';
import { ProjectsStatusFilter } from './projectsStatusFilter';
import { SwitchToggle } from '../formInputs';
import DownloadAsCSV from './downloadAsCSV';
import { GripIcon, ListIcon, FilledNineCellsGridIcon, TableListIcon } from '../svgIcons';
Expand Down Expand Up @@ -151,6 +152,7 @@ const DifficultyDropdown = (props) => {
};

export const ProjectNav = ({ isExploreProjectsPage, children }) => {
const dispatch = useDispatch();
const location = useLocation();
const [fullProjectsQuery, setQuery] = useExploreProjectsQueryParams();
const encodedParams = stringify(fullProjectsQuery)
Expand Down Expand Up @@ -194,6 +196,10 @@ export const ProjectNav = ({ isExploreProjectsPage, children }) => {
if ((isExploreProjectsPage && isExploreProjectsTableView) || !isMapShown) {
clearFiltersURL = './?omitMapResults=1';
}
// fixes clear filter not persisting grid/list view issue
if (searchParams.view) {
clearFiltersURL = `${clearFiltersURL}&view=${searchParams.view}`;
}

// onSelectedItemChange={(changes) => console.log(changes)}
return (
Expand All @@ -205,7 +211,10 @@ export const ProjectNav = ({ isExploreProjectsPage, children }) => {
<div className="mv2 dib">
<DifficultyDropdown setQuery={setQuery} fullProjectsQuery={fullProjectsQuery} />
</div>

<ProjectsActionFilter setQuery={setQuery} fullProjectsQuery={fullProjectsQuery} />

<ProjectsStatusFilter setQuery={setQuery} fullProjectsQuery={fullProjectsQuery} />
<Link
to={filterRouteToggled}
id="more-filter-id"
Expand All @@ -225,7 +234,20 @@ export const ProjectNav = ({ isExploreProjectsPage, children }) => {
className="f6"
/>
{!filterIsEmpty && (
<ClearFilters url={clearFiltersURL} className="mv2 mh1 fr dn dib-l" />
<ClearFilters
url={clearFiltersURL}
className="mv2 mh1 fr dn dib-l"
onClick={() => {
dispatch({ type: 'SET_ACTION', action: 'any' });
setQuery(
{
...fullProjectsQuery,
action: 'any',
},
'pushIn',
);
}}
/>
)}

<ProjectSearchBox
Expand Down
8 changes: 3 additions & 5 deletions frontend/src/components/projects/projectsActionFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,22 @@ export const ProjectsActionFilter = ({ setQuery, fullProjectsQuery }) => {
'pushIn',
);
}
// 'Archived' is a special case, as it is not a valid action
dispatch({ type: 'SET_ACTION', action: value === 'ARCHIVED' ? 'any' : value });
dispatch({ type: 'SET_ACTION', action: value });
setQuery(
{
...fullProjectsQuery,
page: undefined,
status: value !== 'ARCHIVED' ? undefined : 'ARCHIVED',
action: value,
},
'pushIn',
);
}}
// use the action query param, in case someone loads the page with /explore?action=*
value={fullProjectsQuery.status || fullProjectsQuery.action || action || 'any'}
value={fullProjectsQuery.action || action || 'any'}
options={[
{ label: <FormattedMessage {...messages.projectsToMap} />, value: 'map' },
{ label: <FormattedMessage {...messages.projectsToValidate} />, value: 'validate' },
{ label: <FormattedMessage {...messages.anyProject} />, value: 'any' },
{ label: <FormattedMessage {...messages.archived} />, value: 'ARCHIVED' },
]}
display={'Action'}
className={'ba b--tan bg-white mr3 f6 v-mid dn dib-ns pv2 br1 pl3 fw5 blue-dark'}
Expand Down
59 changes: 59 additions & 0 deletions frontend/src/components/projects/projectsStatusFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';

import messages from './messages';
import { Dropdown } from '../dropdown';

const ALL_STATUSES = ['PUBLISHED', 'ARCHIVED', 'DRAFT'];
const ALL = 'all';

export const ProjectsStatusFilter = ({ setQuery, fullProjectsQuery }) => {
const userDetails = useSelector((state) => state.auth.userDetails);
const isOrgManager = useSelector(
(state) => state.auth.organisations && state.auth.organisations.length > 0,
);
// allow admin features for super admin as well as organisation manager
const isAdmin = (userDetails && userDetails.role === 'ADMIN') || isOrgManager;

const dropdownOptions = [
{ label: <FormattedMessage {...messages.allprojects} />, value: ALL },
{ label: <FormattedMessage {...messages.published} />, value: 'PUBLISHED' },
{ label: <FormattedMessage {...messages.archived} />, value: 'ARCHIVED' },
];

if (isAdmin) {
dropdownOptions.push({ label: <FormattedMessage {...messages.draft} />, value: 'DRAFT' });
}

const handleStatusChange = useCallback(
(selected) => {
const value = selected && selected[0] && selected[0].value;
// filter out `DRAFT` for non-admins
const status = isAdmin ? ALL_STATUSES : ALL_STATUSES.filter((stat) => stat !== 'DRAFT');
setQuery(
{
...fullProjectsQuery,
page: undefined,
status: value === ALL ? status.join(',') : value,
},
'pushIn',
);
},
[fullProjectsQuery, setQuery, isAdmin],
);

const statuses = isAdmin ? ALL_STATUSES : ALL_STATUSES.filter((stat) => stat !== 'DRAFT');

return (
<Dropdown
onChange={handleStatusChange}
value={
fullProjectsQuery.status === statuses.join(',') ? ALL : fullProjectsQuery.status || 'any'
}
options={dropdownOptions}
display="Project Status"
className="ba b--tan bg-white mr3 f6 v-mid dn dib-ns pv2 br1 pl3 fw5 blue-dark"
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@ describe('ProjectsActionFilter', () => {
expect(screen.queryByText('Any project')).toBeInTheDocument();
expect(screen.queryByText('Projects to map')).not.toBeInTheDocument();
expect(screen.queryByText('Projects to validate')).not.toBeInTheDocument();
expect(screen.queryByText('Archived')).not.toBeInTheDocument();
// open dropdown
await user.click(screen.queryByText('Any project'));
expect(screen.queryByText('Projects to map')).toBeInTheDocument();
expect(screen.queryByText('Projects to validate')).toBeInTheDocument();
expect(screen.queryByText('Archived')).toBeInTheDocument();
// select Projects to validate
await user.click(screen.queryByText('Projects to validate'));
expect(store.getState()['preferences']['action']).toBe('validate');
Expand All @@ -33,10 +31,6 @@ describe('ProjectsActionFilter', () => {
await user.click(screen.queryByText('Any project'));
await user.click(screen.queryByText('Projects to map'));
expect(store.getState()['preferences']['action']).toBe('map');
// select Projects to archived, action set to any for this special case
await user.click(screen.queryByText('Projects to map'));
await user.click(screen.queryByText(/archived/i));
expect(store.getState()['preferences']['action']).toBe('any');
});

it('initialize it with validate action set', async () => {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/UseProjectsQueryAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const projectQueryAllSpecification = {
partnershipFrom: StringParam,
partnershipTo: StringParam,
downloadAsCSV: BooleanParam,
view: StringParam,
};

/* This can be passed into project API or used independently */
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@
"project.nav.manageProjects": "Manage Projects",
"project.nav.favorited": "Favorited",
"project.nav.contributed": "Contributed",
"project.nav.allprojects": "All",
"project.nav.allprojects": "All Projects",
"project.nav.active": "Active",
"project.nav.stale": "Stale",
"project.nav.managed": "Managed by me",
Expand Down