diff --git a/Dockerfile b/Dockerfile index 74539ee476..9b3a7ede9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -60,6 +60,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PYTHONFAULTHANDLER=1 \ PATH="/home/appuser/.local/bin:$PATH" \ + PYTHONPATH="/usr/src/app:$PYTHONPATH" \ PYTHON_LIB="/home/appuser/.local/lib/python$PYTHON_IMG_TAG/site-packages" \ SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt diff --git a/docker-compose.yml b/docker-compose.yml index 89c2d175bf..e8f0c294f3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,6 +40,7 @@ services: - ./pyproject.toml:/usr/src/app/pyproject.toml:ro - ./backend:/usr/src/app/backend:ro - ./tests:/usr/src/app/tests:ro + - ./migrations:/src/migrations restart: unless-stopped healthcheck: test: curl --fail http://localhost:5000 || exit 1 diff --git a/frontend/src/api/projects.js b/frontend/src/api/projects.js index 03b8af8af0..fc8083477a 100644 --- a/frontend/src/api/projects.js +++ b/frontend/src/api/projects.js @@ -39,7 +39,7 @@ export const useProjectsQuery = (fullProjectsQuery, action, queryOptions) => { }); }; -export const useProjectQuery = (projectId) => { +export const useProjectQuery = (projectId, otherOptions) => { const token = useSelector((state) => state.auth.token); const locale = useSelector((state) => state.preferences['locale']); const fetchProject = ({ signal }) => { @@ -51,6 +51,7 @@ export const useProjectQuery = (projectId) => { return useQuery({ queryKey: ['project', projectId], queryFn: fetchProject, + ...otherOptions, }); }; export const useProjectSummaryQuery = (projectId, otherOptions = {}) => { diff --git a/frontend/src/components/footer/index.js b/frontend/src/components/footer/index.js index 0aa8118eac..e91613ee09 100644 --- a/frontend/src/components/footer/index.js +++ b/frontend/src/components/footer/index.js @@ -36,6 +36,8 @@ export function Footer() { const footerDisabledPaths = [ 'projects/:id/tasks', + 'projects/:id/instructions', + 'projects/:id/contributions', 'projects/:id/map', 'projects/:id/validate', 'projects/:id/live', diff --git a/frontend/src/components/homepage/stats.js b/frontend/src/components/homepage/stats.js index 1901fa35f4..c970ed5ed9 100644 --- a/frontend/src/components/homepage/stats.js +++ b/frontend/src/components/homepage/stats.js @@ -3,6 +3,7 @@ import shortNumber from 'short-number'; import messages from './messages'; import { useOsmStatsQuery, useSystemStatisticsQuery } from '../../api/stats'; +import StatsInfoFooter from '../statsInfoFooter'; export const StatsNumber = (props) => { const value = shortNumber(props.value); @@ -38,8 +39,10 @@ export const StatsSection = () => { const hasStatsLoaded = hasTmStatsLoaded && hasOsmStatsLoaded; return ( - <> -
+
+ + +
{ value={hasStatsLoaded ? tmStatsData.data.mappersOnline : undefined} />
- +
); }; diff --git a/frontend/src/components/partnerMapswipeStats/overview.js b/frontend/src/components/partnerMapswipeStats/overview.js index 0eeaf21818..eb88287fa6 100644 --- a/frontend/src/components/partnerMapswipeStats/overview.js +++ b/frontend/src/components/partnerMapswipeStats/overview.js @@ -30,7 +30,7 @@ export const formatSecondsToTwoUnits = (seconds, shortFormat = false) => { years: 'yrs', months: 'mos', weeks: 'wks', - days: 'ds', + days: 'days', hours: 'hrs', minutes: 'mins', seconds: 'secs', @@ -82,8 +82,11 @@ export const Overview = () => { customPlaceholder={} ready={!isLoading && !isRefetching} > +

+ {data?.nameInsideProvider} +

diff --git a/frontend/src/components/partnerMapswipeStats/swipesByOrganization.js b/frontend/src/components/partnerMapswipeStats/swipesByOrganization.js index 9209740dc3..7a94ae5956 100644 --- a/frontend/src/components/partnerMapswipeStats/swipesByOrganization.js +++ b/frontend/src/components/partnerMapswipeStats/swipesByOrganization.js @@ -80,11 +80,10 @@ export const SwipesByOrganization = ({ contributionsByOrganization = [] }) => { }, []); return ( -
+

-
{contributionsByOrganization.length === 0 && ( diff --git a/frontend/src/components/partnerMapswipeStats/swipesByProjectType.js b/frontend/src/components/partnerMapswipeStats/swipesByProjectType.js index 399c00f608..5d79f22cc6 100644 --- a/frontend/src/components/partnerMapswipeStats/swipesByProjectType.js +++ b/frontend/src/components/partnerMapswipeStats/swipesByProjectType.js @@ -82,7 +82,7 @@ export const SwipesByProjectType = ({ contributionsByProjectType = [] }) => { }, []); return ( -
+

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 (
+ +

{partner.primary_hashtag diff --git a/frontend/src/components/partners/partners.js b/frontend/src/components/partners/partners.js index 3557a5e7d0..2a01708188 100644 --- a/frontend/src/components/partners/partners.js +++ b/frontend/src/components/partners/partners.js @@ -95,7 +95,7 @@ export function PartnersCard({ details }) { - + { className={'w-25-l w-50-m w-100 mv1'} />

-
); }; 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 = ({
-
- - - - + ) : ( + + + + + )}
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 () => {