diff --git a/frontend/src/components/partners/leaderboard.js b/frontend/src/components/partners/leaderboard.js
index aeb2cb7087..c2cddd1920 100644
--- a/frontend/src/components/partners/leaderboard.js
+++ b/frontend/src/components/partners/leaderboard.js
@@ -4,10 +4,13 @@ import messages from '../../views/messages';
import { StatsSection } from './partnersStats';
import { Activity } from './partnersActivity';
import { CurrentProjects } from './currentProjects';
+import StatsInfoFooter from '../statsInfoFooter';
export const Leaderboard = ({ partner, partnerStats }) => {
return (
);
};
diff --git a/frontend/src/components/projectDetail/footer.js b/frontend/src/components/projectDetail/footer.js
index 05f8fe1910..1e66dbe6f6 100644
--- a/frontend/src/components/projectDetail/footer.js
+++ b/frontend/src/components/projectDetail/footer.js
@@ -1,5 +1,5 @@
import { useRef, Fragment } from 'react';
-import { Link } from 'react-router-dom';
+import { Link, useLocation } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { FormattedMessage } from 'react-intl';
@@ -58,6 +58,7 @@ const menuItems = [
export const ProjectDetailFooter = ({ className, projectId }) => {
const userIsloggedIn = useSelector((state) => state.auth.token);
const menuItemsContainerRef = useRef(null);
+ const { pathname } = useLocation();
return (
{
{userIsloggedIn &&
}
-
+
diff --git a/frontend/src/components/projectDetail/index.js b/frontend/src/components/projectDetail/index.js
index c40ab63ad0..3baaa17aad 100644
--- a/frontend/src/components/projectDetail/index.js
+++ b/frontend/src/components/projectDetail/index.js
@@ -1,5 +1,5 @@
-import { lazy, Suspense, useState } from 'react';
-import { Link } from 'react-router-dom';
+import { lazy, Suspense, useState, useEffect } from 'react';
+import { Link, useParams } from 'react-router-dom';
import ReactPlaceholder from 'react-placeholder';
import centroid from '@turf/centroid';
import { FormattedMessage } from 'react-intl';
@@ -35,9 +35,14 @@ import { ENABLE_EXPORT_TOOL } from '../../config/index.js';
/* lazy imports must be last import */
const ProjectTimeline = lazy(() => import('./timeline' /* webpackChunkName: "timeline" */));
-const ProjectDetailMap = (props) => {
+export const ProjectDetailMap = (props) => {
const [taskBordersOnly, setTaskBordersOnly] = useState(true);
+ useEffect(() => {
+ if (typeof props.taskBordersOnly !== 'boolean') return;
+ setTaskBordersOnly(props.taskBordersOnly);
+ }, [props.taskBordersOnly]);
+
const taskBordersGeoJSON = props.project.areaOfInterest && {
type: 'FeatureCollection',
features: [
@@ -139,6 +144,7 @@ export const ProjectDetailLeft = ({ project, contributors, className, type }) =>
export const ProjectDetail = (props) => {
useSetProjectPageTitleTag(props.project);
const size = useWindowSize();
+ const { id: projectId } = useParams();
const { data: contributors, status: contributorsStatus } = useProjectContributionsQuery(
props.project.projectId,
);
@@ -181,6 +187,12 @@ export const ProjectDetail = (props) => {
className="ph4 w-60-l w-80-m w-100 lh-title markdown-content blue-dark-abbey"
dangerouslySetInnerHTML={htmlDescription}
/>
+
+
+
@@ -435,6 +447,7 @@ ProjectDetailMap.propTypes = {
type: PropTypes.string,
tasksError: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
projectLoading: PropTypes.bool,
+ taskBordersOnly: PropTypes.bool,
};
ProjectDetailLeft.propTypes = {
diff --git a/frontend/src/components/projectDetail/messages.js b/frontend/src/components/projectDetail/messages.js
index cd457852c4..bd1826ad37 100644
--- a/frontend/src/components/projectDetail/messages.js
+++ b/frontend/src/components/projectDetail/messages.js
@@ -344,4 +344,8 @@ export default defineMessages({
id: 'project.noSimilarProjectsFound',
defaultMessage: 'Could not find any similar projects for this project',
},
+ viewProjectSpecificInstructions: {
+ id: 'project.viewProjectSpecificInstructions',
+ defaultMessage: 'View project specific instructions',
+ },
});
diff --git a/frontend/src/components/projectDetail/styles.scss b/frontend/src/components/projectDetail/styles.scss
index 41c4a8b923..405ba01659 100644
--- a/frontend/src/components/projectDetail/styles.scss
+++ b/frontend/src/components/projectDetail/styles.scss
@@ -36,3 +36,7 @@
.react-tooltip#dueDateBoxTooltip {
z-index: 999;
}
+
+.project-instructions-link {
+ letter-spacing: -0.0857513px;
+}
diff --git a/frontend/src/components/projectStats/edits.js b/frontend/src/components/projectStats/edits.js
index 9059c5ba76..b3bdb46d11 100644
--- a/frontend/src/components/projectStats/edits.js
+++ b/frontend/src/components/projectStats/edits.js
@@ -4,7 +4,7 @@ import projectMessages from './messages';
import userDetailMessages from '../userDetail/messages';
import { MappingIcon, HomeIcon, RoadIcon, EditIcon } from '../svgIcons';
import { StatsCard } from '../statsCard';
-import StatsTimestamp from '../statsTimestamp';
+import StatsInfoFooter from '../statsInfoFooter';
export const EditsStats = ({ data }) => {
const { changesets, buildings, roads, edits } = data;
@@ -18,30 +18,32 @@ export const EditsStats = ({ data }) => {
-
-
-
}
- description={
}
- value={changesets || 0}
- />
-
}
- description={
}
- value={edits || 0}
- />
-
}
- description={
}
- value={buildings || 0}
- />
-
}
- description={
}
- value={roads || 0}
- />
+
+
+
+ }
+ description={ }
+ value={changesets || 0}
+ />
+ }
+ description={ }
+ value={edits || 0}
+ />
+ }
+ description={ }
+ value={buildings || 0}
+ />
+ }
+ description={ }
+ value={roads || 0}
+ />
+
);
diff --git a/frontend/src/components/statsInfoFooter/index.jsx b/frontend/src/components/statsInfoFooter/index.jsx
index abe69a640a..b3cf5a2ea3 100644
--- a/frontend/src/components/statsInfoFooter/index.jsx
+++ b/frontend/src/components/statsInfoFooter/index.jsx
@@ -2,27 +2,37 @@ import { useIntl } from 'react-intl';
import { useOsmStatsMetadataQuery } from '../../api/stats';
import { dateOptions } from '../statsTimestamp';
+import { InfoIcon } from '../svgIcons';
+import '../../views/partnersMapswipeStats.scss';
-export default function StatsInfoFooter() {
+export default function StatsInfoFooter({ className }) {
const intl = useIntl();
const { data: osmStatsMetadata } = useOsmStatsMetadataQuery();
return (
-
-
- These statistics come from{' '}
-
- ohsomeNow Stats
- {' '}
- and were last updated at{' '}
- {intl.formatDate(osmStatsMetadata?.max_timestamp, dateOptions)} (
- {intl.timeZone}).
+
+
+
+
+ These statistics come from{' '}
+
+ ohsomeNow Stats
+ {' '}
+ and were last updated at{' '}
+
+ {intl.formatDate(osmStatsMetadata?.max_timestamp, dateOptions)}
+ {' '}
+ ({intl.timeZone})
+
);
diff --git a/frontend/src/components/taskSelection/footer.js b/frontend/src/components/taskSelection/footer.js
index 03e6113dfe..821a4cfc21 100644
--- a/frontend/src/components/taskSelection/footer.js
+++ b/frontend/src/components/taskSelection/footer.js
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate, useLocation } from 'react-router-dom';
import Popup from 'reactjs-popup';
import { FormattedMessage } from 'react-intl';
@@ -24,6 +24,7 @@ const TaskSelectionFooter = ({
setSelectedTasks,
}) => {
const navigate = useNavigate();
+ const { pathname } = useLocation();
const token = useSelector((state) => state.auth.token);
const locale = useSelector((state) => state.preferences.locale);
const [editor, setEditor] = useState(defaultUserEditor);
@@ -221,7 +222,20 @@ const TaskSelectionFooter = ({
- lockTasks()} loading={isPending}>
+ {
+ if (!token) {
+ navigate('/login', {
+ // for redirecting to the same page after login
+ state: { from: pathname },
+ });
+ } else {
+ lockTasks();
+ }
+ }}
+ loading={isPending}
+ >
{['selectAnotherProject', 'mappingIsComplete', 'projectIsComplete'].includes(
taskAction,
) || project.status === 'ARCHIVED' ? (
diff --git a/frontend/src/components/taskSelection/index.js b/frontend/src/components/taskSelection/index.js
index 6803d5f24b..650f035604 100644
--- a/frontend/src/components/taskSelection/index.js
+++ b/frontend/src/components/taskSelection/index.js
@@ -1,5 +1,5 @@
-import { lazy, useState, useEffect, Suspense } from 'react';
-import { useLocation } from 'react-router-dom';
+import { lazy, useState, useEffect, useCallback, Suspense, useRef } from 'react';
+import { useLocation, useParams, useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { useQueryParam, StringParam } from 'use-query-params';
import Popup from 'reactjs-popup';
@@ -19,6 +19,7 @@ import { TasksMapLegend } from './legend';
import { ProjectInstructions } from './instructions';
import { ChangesetCommentTags } from './changesetComment';
import { ProjectHeader } from '../projectDetail/header';
+import { ProjectDetailMap } from '../projectDetail';
import Contributions from './contributions';
import { UserPermissionErrorContent } from './permissionErrorModal';
import { Alert } from '../alert';
@@ -31,6 +32,7 @@ import {
useTasksQuery,
} from '../../api/projects';
import { useTeamsQuery } from '../../api/teams';
+
const TaskSelectionFooter = lazy(() => import('./footer'));
const getRandomTaskByAction = (activities, taskAction) => {
@@ -53,19 +55,22 @@ const getRandomTaskByAction = (activities, taskAction) => {
export function TaskSelection({ project }: Object) {
useSetProjectPageTitleTag(project);
const { projectId } = project;
+ const { tabname: activeSection } = useParams();
+ const navigate = useNavigate();
const location = useLocation();
const dispatch = useDispatch();
const user = useSelector((state) => state.auth.userDetails);
+ const token = useSelector((state) => state.auth.token);
const userOrgs = useSelector((state) => state.auth.organisations);
const lockedTasks = useGetLockedTasks();
const [zoomedTaskId, setZoomedTaskId] = useState(null);
- const [activeSection, setActiveSection] = useState(null);
const [selected, setSelectedTasks] = useState([]);
const [mapInit, setMapInit] = useState(false);
const [taskAction, setTaskAction] = useState('mapATask');
const [activeStatus, setActiveStatus] = useState(null);
const [activeUser, setActiveUser] = useState(null);
const [textSearch, setTextSearch] = useQueryParam('search', StringParam);
+ const isFirstRender = useRef(true); // to check if component is rendered first time
const { data: userTeams, isLoading: isUserTeamsLoading } = useTeamsQuery(
{
@@ -74,6 +79,7 @@ export function TaskSelection({ project }: Object) {
},
{
useErrorBoundary: true,
+ enabled: !!token,
},
);
const { data: activities, refetch: getActivities } = useActivitiesQuery(projectId);
@@ -87,6 +93,7 @@ export function TaskSelection({ project }: Object) {
// Task status on the map were not being updated when coming from the action page,
// so added this as a workaround.
cacheTime: 0,
+ enabled: false,
});
const {
data: priorityAreas,
@@ -114,23 +121,46 @@ export function TaskSelection({ project }: Object) {
// update tasks geometry if there are new tasks (caused by task splits)
// update tasks state (when activities have changed)
useEffect(() => {
- if (tasksData?.features.length !== activities?.activity.length) {
+ if (tasksData?.features.length !== activities?.activity.length && token) {
refetchTasks();
}
- }, [tasksData, activities, refetchTasks]);
+ }, [tasksData, activities, refetchTasks, token]);
+
+ // use route instead of local state for active tab states
+ const setActiveSection = useCallback(
+ (section) => {
+ if (!!textSearch) return; // if search param not present, do not set active section
+ navigate(`/projects/${projectId}/${section}`);
+ },
+ [navigate, projectId, textSearch],
+ );
+
+ // remove history location state since react-router-dom persists state on reload
+ useEffect(() => {
+ function onBeforeUnload() {
+ window.history.replaceState({}, '');
+ }
+ window.addEventListener('beforeunload', onBeforeUnload);
+ return () => {
+ window.removeEventListener('beforeunload', onBeforeUnload);
+ };
+ }, []);
// show the tasks tab when the page loads if the user has already contributed
// to the project. If no, show the instructions tab.
useEffect(() => {
- if (contributions && activeSection === null) {
+ // do not redirect if user is not from project detail page
+ if (location?.state?.from !== `/projects/${projectId}`) return;
+ if (contributions && isFirstRender.current) {
const currentUserContributions = contributions.filter((u) => u.username === user.username);
if (textSearch || (user.isExpert && currentUserContributions.length > 0)) {
setActiveSection('tasks');
} else {
setActiveSection('instructions');
}
+ isFirstRender.current = false;
}
- }, [contributions, user.username, user, activeSection, textSearch]);
+ }, [contributions, user.username, user, textSearch, setActiveSection, location, projectId]);
useEffect(() => {
// run it only when the component is initialized
@@ -291,6 +321,7 @@ export function TaskSelection({ project }: Object) {
>
) : null}
+
{activeSection === 'contributions' ? (
-
-
-
-
+ ) : (
+
+
+
+
+ )}
diff --git a/frontend/src/components/taskSelection/tabSelector.js b/frontend/src/components/taskSelection/tabSelector.js
index fbc67be72f..c996258e7d 100644
--- a/frontend/src/components/taskSelection/tabSelector.js
+++ b/frontend/src/components/taskSelection/tabSelector.js
@@ -1,19 +1,25 @@
import { FormattedMessage } from 'react-intl';
+import { useSelector } from 'react-redux';
import messages from './messages';
-export const TabSelector = ({ activeSection, setActiveSection }) => (
-
- {['tasks', 'instructions', 'contributions'].map((section) => (
-
setActiveSection(section)}
- >
-
-
- ))}
-
-);
+export const TabSelector = ({ activeSection, setActiveSection }) => {
+ const token = useSelector((state) => state.auth.token);
+ const tabs = token ? ['tasks', 'instructions', 'contributions'] : ['instructions'];
+
+ return (
+
+ {tabs.map((section) => (
+
setActiveSection(section)}
+ >
+
+
+ ))}
+
+ );
+};
diff --git a/frontend/src/components/taskSelection/tests/footer.test.js b/frontend/src/components/taskSelection/tests/footer.test.js
index 16bdf9d51c..7b165c8648 100644
--- a/frontend/src/components/taskSelection/tests/footer.test.js
+++ b/frontend/src/components/taskSelection/tests/footer.test.js
@@ -144,6 +144,7 @@ describe('Footer Lock Tasks', () => {
store.dispatch({ type: 'SET_PROJECT', project: null });
store.dispatch({ type: 'SET_LOCKED_TASKS', tasks: [] });
store.dispatch({ type: 'SET_TASKS_STATUS', status: null });
+ store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
});
it('should display task cannot be locked for mapping message', async () => {
diff --git a/frontend/src/components/taskSelection/tests/index.test.js b/frontend/src/components/taskSelection/tests/index.test.js
index e3d8302e90..e069d95ed2 100644
--- a/frontend/src/components/taskSelection/tests/index.test.js
+++ b/frontend/src/components/taskSelection/tests/index.test.js
@@ -1,21 +1,45 @@
import '@testing-library/jest-dom';
-import { screen, act, waitFor } from '@testing-library/react';
+import { render, screen, act, waitFor } from '@testing-library/react';
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
import { QueryParamProvider } from 'use-query-params';
+import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import userEvent from '@testing-library/user-event';
import { TaskSelection } from '..';
import { getProjectSummary } from '../../../network/tests/mockData/projects';
-import {
- QueryClientProviders,
- ReduxIntlProviders,
- renderWithRouter,
-} from '../../../utils/testWithIntl';
+import { QueryClientProviders, ReduxIntlProviders } from '../../../utils/testWithIntl';
import { store } from '../../../store';
describe('Contributions', () => {
+ const setup = () => {
+ return {
+ user: userEvent.setup(),
+ ...render(
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+ ,
+ ),
+ };
+ };
+
it('should select tasks mapped by the selected user', async () => {
act(() => {
- store.dispatch({ type: 'SET_TOKEN', token: null });
+ store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
store.dispatch({ type: 'SET_LOCALE', locale: 'en-US' });
store.dispatch({
type: 'SET_USER_DETAILS',
@@ -23,16 +47,7 @@ describe('Contributions', () => {
});
});
- const { user } = renderWithRouter(
-
-
-
-
-
-
- ,
- );
-
+ const { user } = setup();
await waitFor(() =>
expect(screen.getByText(/Project Specific Mapping Notes/i)).toBeInTheDocument(),
);
@@ -54,16 +69,7 @@ describe('Contributions', () => {
});
it('should select tasks validated by the selected user', async () => {
- const { user } = renderWithRouter(
-
-
-
-
-
-
- ,
- );
-
+ const { user } = setup();
await waitFor(() =>
expect(screen.getByText(/Project Specific Mapping Notes/i)).toBeInTheDocument(),
);
@@ -85,16 +91,7 @@ describe('Contributions', () => {
});
it('should sort tasks by their task number', async () => {
- const { user } = renderWithRouter(
-
-
-
-
-
-
- ,
- );
-
+ const { user } = setup();
await waitFor(() =>
expect(screen.getByText(/Project Specific Mapping Notes/i)).toBeInTheDocument(),
);
@@ -120,16 +117,7 @@ describe('Contributions', () => {
});
it('should clear text when close icon is clicked', async () => {
- const { user } = renderWithRouter(
-
-
-
-
-
-
- ,
- );
-
+ const { user } = setup();
await waitFor(() =>
expect(screen.getByText(/Project Specific Mapping Notes/i)).toBeInTheDocument(),
);
diff --git a/frontend/src/components/taskSelection/tests/tabSelector.test.js b/frontend/src/components/taskSelection/tests/tabSelector.test.js
index cafd75648a..2f22fc309d 100644
--- a/frontend/src/components/taskSelection/tests/tabSelector.test.js
+++ b/frontend/src/components/taskSelection/tests/tabSelector.test.js
@@ -1,14 +1,20 @@
import '@testing-library/jest-dom';
-import { render, screen } from '@testing-library/react';
+import { render, screen, act } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import { ReduxIntlProviders } from '../../../utils/testWithIntl';
import { TabSelector } from '../tabSelector';
-import userEvent from '@testing-library/user-event';
+import { store } from '../../../store';
describe('TabSelector component', () => {
const setActiveSection = jest.fn();
+ const user = userEvent.setup();
+
+ act(() => {
+ store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
+ });
+
it('with the tasks tab active', async () => {
- const user = userEvent.setup();
const { container } = render(
@@ -23,8 +29,8 @@ describe('TabSelector component', () => {
await user.click(screen.getByText('Instructions'));
expect(setActiveSection).toHaveBeenCalledWith('instructions');
});
+
it('with the instructions tab active', async () => {
- const user = userEvent.setup();
render(
@@ -36,8 +42,8 @@ describe('TabSelector component', () => {
await user.click(screen.getByText('contributions'));
expect(setActiveSection).toHaveBeenLastCalledWith('contributions');
});
+
it('with the contributions tab active', async () => {
- const user = userEvent.setup();
render(
diff --git a/frontend/src/components/teamsAndOrgs/featureStats.js b/frontend/src/components/teamsAndOrgs/featureStats.js
index b5b31ca9c3..5522e0f66a 100644
--- a/frontend/src/components/teamsAndOrgs/featureStats.js
+++ b/frontend/src/components/teamsAndOrgs/featureStats.js
@@ -7,7 +7,7 @@ import userDetailMessages from '../userDetail/messages';
import { OHSOME_STATS_BASE_URL, defaultChangesetComment } from '../../config';
import { RoadIcon, HomeIcon, WavesIcon, MarkerIcon } from '../svgIcons';
import { StatsCard } from '../statsCard';
-import StatsTimestamp from '../statsTimestamp';
+import StatsInfoFooter from '../statsInfoFooter';
export const FeatureStats = () => {
const [stats, setStats] = useState({ edits: 0, buildings: 0, roads: 0, pois: 0, waterways: 0 });
@@ -42,33 +42,36 @@ export const FeatureStats = () => {
-
-
}
- description={
}
- value={stats.buildings || 0}
- className={'w-25-l w-50-m w-100 mv1'}
- />
-
}
- description={
}
- value={stats.roads || 0}
- className={'w-25-l w-50-m w-100 mv1'}
- />
-
}
- description={
}
- value={stats.pois || 0}
- className={'w-25-l w-50-m w-100 mv1'}
- />
-
}
- description={
}
- value={stats.waterways || 0}
- className={'w-25-l w-50-m w-100 mv1'}
- />
+
+
+
+ }
+ description={ }
+ value={stats.buildings || 0}
+ className={'w-25-l w-50-m w-100 mv1'}
+ />
+ }
+ description={ }
+ value={stats.roads || 0}
+ className={'w-25-l w-50-m w-100 mv1'}
+ />
+ }
+ description={ }
+ value={stats.pois || 0}
+ className={'w-25-l w-50-m w-100 mv1'}
+ />
+ }
+ description={ }
+ value={stats.waterways || 0}
+ className={'w-25-l w-50-m w-100 mv1'}
+ />
+
>
);
diff --git a/frontend/src/components/userDetail/elementsMapped.js b/frontend/src/components/userDetail/elementsMapped.js
index 1e1655c56f..6dc4b3bc31 100644
--- a/frontend/src/components/userDetail/elementsMapped.js
+++ b/frontend/src/components/userDetail/elementsMapped.js
@@ -125,6 +125,8 @@ export const ElementsMapped = ({ userStats, osmStats }) => {
return (
+
+
{
unitLess={osmStats?.waterway?.modified?.unit_less}
/>
-
-
);
};
diff --git a/frontend/src/hooks/UseFilterContributors.js b/frontend/src/hooks/UseFilterContributors.js
index 4b9811c8c1..5e7789b074 100644
--- a/frontend/src/hooks/UseFilterContributors.js
+++ b/frontend/src/hooks/UseFilterContributors.js
@@ -6,7 +6,7 @@ export function useFilterContributors(contributors, level, username) {
const [filteredContributors, setFilter] = useState([]);
useEffect(() => {
- let users = contributors;
+ let users = contributors || [];
if (['ADVANCED', 'INTERMEDIATE', 'BEGINNER'].includes(level)) {
users = users.filter((user) => user.mappingLevel === level);
}
diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json
index f9b8216015..70eb86236f 100644
--- a/frontend/src/locales/fr.json
+++ b/frontend/src/locales/fr.json
@@ -92,7 +92,7 @@
"serviceWorker.dialog.newVersion": "Un nouveau Gestionnaire de Tâches est disponible !",
"serviceWorker.dialog.update": "Mettre à jour maintenant",
"serviceWorker.dialog.remindMeLater": "Rappelez-moi plus tard",
- "home.mainSection.title": "Cartographier pour les personnes en diffculté",
+ "home.mainSection.title": "Cartographier pour les personnes en difficulté",
"home.mainSection.lead": "Faites partie d'une communauté mondiale cartographiant les populations et lieux les plus vulnérables au monde quant aux désastres et la pauvreté pour soutenir l'aide humanitaire et le développement durable à travers le monde.",
"home.callToAction.title": "Nous ne pouvons pas le faire sans vous",
"home.callToAction.firstLeadLine": "Tout le monde peut contribuer à la carte. Si vous n'avez jamais cartographié auparavant et vous souhaitez commencer, visitez notre page {link}. ",
diff --git a/frontend/src/routes.js b/frontend/src/routes.js
index 7495186182..3647b56f87 100644
--- a/frontend/src/routes.js
+++ b/frontend/src/routes.js
@@ -54,7 +54,7 @@ export const router = createBrowserRouter(
}}
/>
{
const { SelectTask } = await import(
'./views/taskSelection' /* webpackChunkName: "taskSelection" */
diff --git a/frontend/src/views/home.js b/frontend/src/views/home.js
index 2d423e0b89..7b40ac7c5c 100644
--- a/frontend/src/views/home.js
+++ b/frontend/src/views/home.js
@@ -8,7 +8,6 @@ import { WhoIsMapping } from '../components/homepage/whoIsMapping';
import { Testimonials } from '../components/homepage/testimonials';
import { Alert } from '../components/alert';
import homeMessages from '../components/homepage/messages';
-import StatsTimestamp from '../components/statsTimestamp/';
export function Home() {
return (
@@ -24,9 +23,6 @@ export function Home() {
}
>
-
-
-
diff --git a/frontend/src/views/partnersMapswipeStats.css b/frontend/src/views/partnersMapswipeStats.css
deleted file mode 100644
index 3bf7e57d23..0000000000
--- a/frontend/src/views/partnersMapswipeStats.css
+++ /dev/null
@@ -1,17 +0,0 @@
-.mapswipe-stats-info-banner {
- background-color: #d9d7d7;
- width: fit-content;
- margin-left: 20px;
- border-radius: 3px;
-}
-
-.mapswipe-stats-info-banner::before {
- content: '';
- position: absolute;
- left: -20px;
- top: 0;
- bottom: 0;
- width: 20px;
- background-color: #d9d7d7;
- clip-path: polygon(100% 0, 0 50%, 100% 100%);
-}
diff --git a/frontend/src/views/partnersMapswipeStats.js b/frontend/src/views/partnersMapswipeStats.js
index c9015d3640..b3b097b804 100644
--- a/frontend/src/views/partnersMapswipeStats.js
+++ b/frontend/src/views/partnersMapswipeStats.js
@@ -19,7 +19,7 @@ import { SwipesByProjectType } from '../components/partnerMapswipeStats/swipesBy
import { SwipesByOrganization } from '../components/partnerMapswipeStats/swipesByOrganization';
import messages from './messages';
import { fetchLocalJSONAPI } from '../network/genericJSONRequest';
-import './partnersMapswipeStats.css';
+import './partnersMapswipeStats.scss';
const PagePlaceholder = () => (
@@ -46,7 +46,7 @@ const PagePlaceholder = () => (
const InfoBanner = () => {
return (
-
+
@@ -141,16 +141,16 @@ export const PartnersMapswipeStats = () => {
-
+
{getSwipes()}
-
+
{getTimeSpentContributing()}
@@ -158,7 +158,7 @@ export const PartnersMapswipeStats = () => {
-
+
{
const [partnerStats, setPartnerStats] = useState(null);
const [error, loading, partner] = useFetch(`partners/${id}/`);
- // navigate to /leaderboard path when no tab param present
- useEffect(() => {
- if (!tabname) {
- navigate('leaderboard');
- }
- }, [navigate, tabname]);
-
const fetchData = async (name) => {
try {
let hashtag = name.trim();
@@ -73,12 +66,10 @@ export const PartnersStats = () => {
function getTabContent() {
switch (tabname) {
- case 'leaderboard':
- return ;
case 'mapswipe':
return ;
default:
- return <>>;
+ return ;
}
}
@@ -86,6 +77,13 @@ export const PartnersStats = () => {
.filter((key) => key.startsWith('link'))
.filter((link) => partner[link]);
+ // remove Map Swipe tab if mapswipe_group_id not present
+ const modifiedTabData = !partner?.mapswipe_group_id
+ ? tabData.filter((tab) => tab.id !== 'mapswipe')
+ : tabData;
+
+ const activeTab = tabname === 'mapswipe' ? 'mapswipe' : 'leaderboard';
+
return (
{
)}
- {tabData.map(({ id: tabId, title }) => (
+ {modifiedTabData.map(({ id: tabId, title }) => (
navigate(`/partners/${id}/stats/${tabId}`)}
+ onClick={() =>
+ tabId === 'leaderboard'
+ ? navigate(`/partners/${id}/stats`)
+ : navigate(`/partners/${id}/stats/${tabId}`)
+ }
onKeyDown={() => {}}
>
{title}
diff --git a/frontend/src/views/taskSelection.js b/frontend/src/views/taskSelection.js
index 2b8cfe7f9b..7946bfde65 100644
--- a/frontend/src/views/taskSelection.js
+++ b/frontend/src/views/taskSelection.js
@@ -1,35 +1,63 @@
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
-import { useNavigate, useParams } from 'react-router-dom';
+import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { TaskSelection } from '../components/taskSelection';
import { NotFound } from './notFound';
-import { useProjectSummaryQuery } from '../api/projects';
+import { useProjectSummaryQuery, useProjectQuery } from '../api/projects';
import { Preloader } from '../components/preloader';
+import PrivateProjectError from '../components/projectDetail/privateProjectError';
+
+const publicRoutes = ['/instructions'];
export function SelectTask() {
const { id } = useParams();
+ const { pathname } = useLocation();
const navigate = useNavigate();
const token = useSelector((state) => state.auth.token);
- const { data, status, error } = useProjectSummaryQuery(id, {
+ const {
+ data: projectSummaryData,
+ error: projectSummaryError,
+ status: projectSummaryStatus,
+ } = useProjectSummaryQuery(id, {
useErrorBoundary: (error) => error.response.status !== 404,
+ enabled: !!token,
+ });
+ const {
+ data: projectData,
+ error: projectError,
+ status: projectStatus,
+ } = useProjectQuery(id, {
+ enabled: !token,
});
useEffect(() => {
- if (!token) {
+ const isPublicRoute = publicRoutes.some((url) => pathname.includes(url));
+ if (!isPublicRoute && !token) {
navigate('/login');
}
- }, [navigate, token]);
+ }, [navigate, token, pathname]);
+
+ const status = token ? projectSummaryStatus : projectStatus;
+ const error = token ? projectSummaryError : projectError;
if (status === 'loading') {
return
;
}
if (status === 'error') {
- if (error.response.status === 404) {
- return
;
- }
+ return (
+ <>
+ {error.response.data.SubCode === 'PrivateProject' ? (
+
+ ) : (
+
+ )}
+ >
+ );
}
- return
;
+ const project = token ? projectSummaryData : projectData?.data;
+
+ return
;
}
diff --git a/frontend/src/views/tests/taskSelection.test.js b/frontend/src/views/tests/taskSelection.test.js
index 1d5c53eb0f..c81d0f1a88 100644
--- a/frontend/src/views/tests/taskSelection.test.js
+++ b/frontend/src/views/tests/taskSelection.test.js
@@ -17,10 +17,12 @@ describe('Task Selection Page', () => {
return {
user: userEvent.setup(),
...render(
-
+
@@ -54,6 +56,7 @@ describe('Task Selection Page', () => {
userDetails: { id: 69, username: 'user_3', isExpert: false, role: 'READ_ONLY' },
});
store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
+ store.dispatch({ type: 'SET_LOCALE', locale: 'en-US' });
});
setup();
@@ -72,6 +75,7 @@ describe('Task Selection Page', () => {
userDetails: { id: 69, username: 'user_3', isExpert: false },
});
store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
+ store.dispatch({ type: 'SET_LOCALE', locale: 'en-US' });
});
setup();
@@ -84,6 +88,7 @@ describe('Task Selection Page', () => {
type: 'SET_USER_DETAILS',
userDetails: { id: 69, username: 'user_3', isExpert: true },
});
+ store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
});
setup();
await waitFor(() => expect(screen.getByRole('link')).toBeInTheDocument());
@@ -93,6 +98,10 @@ describe('Task Selection Page', () => {
});
it('should change the button text to map selected task when user selects a task', async () => {
+ act(() => {
+ store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
+ store.dispatch({ type: 'SET_LOCALE', locale: 'en-US' });
+ });
const { user } = setup();
await screen.findAllByText(/last updated by/i);
expect(
@@ -130,6 +139,10 @@ describe('Task Selection Page', () => {
});
it('should change the button text to map another task when user selects a task for validation but the user level is not met', async () => {
+ act(() => {
+ store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
+ store.dispatch({ type: 'SET_LOCALE', locale: 'en-US' });
+ });
const { user } = setup();
await screen.findAllByText(/last updated by/i);
// Selecting a task that is available for validation
@@ -151,6 +164,8 @@ describe('Task Selection Page', () => {
type: 'SET_USER_DETAILS',
userDetails: { id: 69, username: 'user_3', isExpert: true, role: 'ADMIN' },
});
+ store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
+ store.dispatch({ type: 'SET_LOCALE', locale: 'en-US' });
});
const { user } = setup();
await screen.findAllByText(/last updated by/i);
@@ -200,6 +215,10 @@ describe('Task Selection Page', () => {
});
it('should filter the task list by search query', async () => {
+ act(() => {
+ store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
+ store.dispatch({ type: 'SET_LOCALE', locale: 'en-US' });
+ });
const { user } = setup();
await screen.findAllByText(/last updated by/i);
expect(
@@ -219,6 +238,10 @@ describe('Task Selection Page', () => {
});
it('should navigate to the contributions tab', async () => {
+ act(() => {
+ store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
+ store.dispatch({ type: 'SET_LOCALE', locale: 'en-US' });
+ });
const { user } = setup();
await screen.findAllByText(/last updated by/i);
await user.click(
@@ -239,10 +262,10 @@ describe('Random Task Selection', () => {
return {
user: userEvent.setup(),
...render(
-
+
@@ -265,6 +288,8 @@ describe('Random Task Selection', () => {
type: 'SET_USER_DETAILS',
userDetails: { id: 69, username: 'user_3', isExpert: true },
});
+ store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
+ store.dispatch({ type: 'SET_LOCALE', locale: 'en-US' });
});
const { user } = setup();
await screen.findAllByText(/last updated by/i);
@@ -293,8 +318,8 @@ describe('Random Task Selection', () => {
userDetails: { id: 69, username: 'user_3', isExpert: false },
});
store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
+ store.dispatch({ type: 'SET_LOCALE', locale: 'en-US' });
});
-
setup();
expect(await screen.findByText(/Project Specific Mapping Notes/i)).toBeInTheDocument();
expect(
@@ -309,7 +334,7 @@ describe('Complete Project', () => {
@@ -325,6 +350,10 @@ describe('Complete Project', () => {
);
it('should display button to select another project', async () => {
+ act(() => {
+ store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
+ store.dispatch({ type: 'SET_LOCALE', locale: 'en-US' });
+ });
setup();
await screen.findAllByText(/last updated by/i);
expect(
@@ -341,7 +370,7 @@ describe('Mapped Project', () => {
@@ -363,6 +392,8 @@ describe('Mapped Project', () => {
userDetails: { id: 69, username: 'user_3', isExpert: false, role: 'ADMIN' },
});
});
+ store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
+ store.dispatch({ type: 'SET_LOCALE', locale: 'en-US' });
setup();
await screen.findAllByText(/last updated by/i);
expect(
@@ -379,7 +410,7 @@ describe('Resume Mapping', () => {
@@ -400,6 +431,8 @@ describe('Resume Mapping', () => {
type: 'SET_USER_DETAILS',
userDetails: { id: 69, username: 'Patrik_B', isExpert: false, role: 'ADMIN' },
});
+ store.dispatch({ type: 'SET_TOKEN', token: 'validToken' });
+ store.dispatch({ type: 'SET_LOCALE', locale: 'en-US' });
});
setup();
await screen.findAllByText(/last updated by/i);
@@ -425,7 +458,7 @@ test('it should pre select task from the list from URL params', async () => {