From 571f76df60c0487f8e88921d2df6ba0760ec1d70 Mon Sep 17 00:00:00 2001 From: royallsilwallz Date: Mon, 23 Sep 2024 11:13:32 +0545 Subject: [PATCH 01/13] Create different route paths for `Instructions` & `Contributions` - Add `tabname` param to `/projects/:id/` route - Change local tab state to route param tabname - Remove footer for instructions & contributions route --- frontend/src/components/footer/index.js | 2 ++ frontend/src/components/taskSelection/index.js | 17 +++++++++++++---- frontend/src/routes.js | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) 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/taskSelection/index.js b/frontend/src/components/taskSelection/index.js index 6803d5f24b..f12860906e 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 } 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'; @@ -53,13 +53,14 @@ 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 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'); @@ -119,6 +120,14 @@ export function TaskSelection({ project }: Object) { } }, [tasksData, activities, refetchTasks]); + // use route instead of local state for active tab states + const setActiveSection = useCallback( + (section) => { + navigate(`/projects/${projectId}/${section}`); + }, + [navigate, projectId], + ); + // show the tasks tab when the page loads if the user has already contributed // to the project. If no, show the instructions tab. useEffect(() => { @@ -130,7 +139,7 @@ export function TaskSelection({ project }: Object) { setActiveSection('instructions'); } } - }, [contributions, user.username, user, activeSection, textSearch]); + }, [contributions, user.username, user, activeSection, textSearch, setActiveSection]); useEffect(() => { // run it only when the component is initialized 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" */ From ca077ffe8a179d0810909ea0009b25cd3ded27bc Mon Sep 17 00:00:00 2001 From: royallsilwallz Date: Mon, 23 Sep 2024 15:32:20 +0545 Subject: [PATCH 02/13] Add button to navigate to `Instructions` page from `Project Detail` page --- frontend/src/components/projectDetail/index.js | 9 ++++++++- frontend/src/components/projectDetail/messages.js | 4 ++++ frontend/src/components/projectDetail/styles.scss | 4 ++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/projectDetail/index.js b/frontend/src/components/projectDetail/index.js index c40ab63ad0..5a1d22fe26 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 { Link, useParams } from 'react-router-dom'; import ReactPlaceholder from 'react-placeholder'; import centroid from '@turf/centroid'; import { FormattedMessage } from 'react-intl'; @@ -139,6 +139,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 +182,12 @@ export const ProjectDetail = (props) => { className="ph4 w-60-l w-80-m w-100 lh-title markdown-content blue-dark-abbey" dangerouslySetInnerHTML={htmlDescription} /> + + + 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; +} From e372d2d5857b15051614156b3d2800ab9a2a37d7 Mon Sep 17 00:00:00 2001 From: royallsilwallz Date: Tue, 8 Oct 2024 11:34:17 +0545 Subject: [PATCH 03/13] Show Project Detail Map instead of Task Map when not logged in - Enable task api call only when logged in - Add `taskBordersOnly` props for directly zooming to tasks in `ProjectDetailMap` - Zoom to task border initially --- frontend/src/api/projects.js | 3 +- .../src/components/projectDetail/index.js | 10 +- .../src/components/taskSelection/index.js | 92 +++++++++++-------- frontend/src/views/taskSelection.js | 27 +++++- 4 files changed, 88 insertions(+), 44 deletions(-) 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/projectDetail/index.js b/frontend/src/components/projectDetail/index.js index 5a1d22fe26..27ffddc9f9 100644 --- a/frontend/src/components/projectDetail/index.js +++ b/frontend/src/components/projectDetail/index.js @@ -1,4 +1,4 @@ -import { lazy, Suspense, useState } from 'react'; +import { lazy, Suspense, useState, useEffect } from 'react'; import { Link, useParams } from 'react-router-dom'; import ReactPlaceholder from 'react-placeholder'; import centroid from '@turf/centroid'; @@ -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: [ @@ -442,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/taskSelection/index.js b/frontend/src/components/taskSelection/index.js index f12860906e..e6500b7bb8 100644 --- a/frontend/src/components/taskSelection/index.js +++ b/frontend/src/components/taskSelection/index.js @@ -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) => { @@ -58,6 +60,7 @@ export function TaskSelection({ project }: Object) { 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); @@ -75,6 +78,7 @@ export function TaskSelection({ project }: Object) { }, { useErrorBoundary: true, + enabled: !!token, }, ); const { data: activities, refetch: getActivities } = useActivitiesQuery(projectId); @@ -88,6 +92,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, @@ -115,10 +120,10 @@ 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( @@ -272,20 +277,22 @@ export function TaskSelection({ project }: Object) {
-
- -
+ {activeSection && ( +
+ +
+ )} {activeSection === 'instructions' ? ( <> {project.enforceRandomTaskSelection && ( @@ -300,6 +307,7 @@ export function TaskSelection({ project }: Object) { ) : null} + {activeSection === 'contributions' ? (
- - - - + ) : ( + + + + + )}
diff --git a/frontend/src/views/taskSelection.js b/frontend/src/views/taskSelection.js index 2b8cfe7f9b..03db202898 100644 --- a/frontend/src/views/taskSelection.js +++ b/frontend/src/views/taskSelection.js @@ -1,25 +1,40 @@ 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'; export function SelectTask() { const { id } = useParams(); 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) { navigate('/login'); } - }, [navigate, token]); + }, [navigate, token, pathname]); + + const status = token ? projectSummaryStatus : projectStatus; + const error = token ? projectSummaryError : projectError; if (status === 'loading') { return ; @@ -31,5 +46,7 @@ export function SelectTask() { } } - return ; + const project = token ? projectSummaryData : projectData.data; + + return ; } From 8b7da1b29921992d11be58949e40569ca7c703fb Mon Sep 17 00:00:00 2001 From: royallsilwallz Date: Tue, 8 Oct 2024 12:48:57 +0545 Subject: [PATCH 04/13] Change `View Instructions` button to link --- frontend/src/components/projectDetail/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/projectDetail/index.js b/frontend/src/components/projectDetail/index.js index 27ffddc9f9..3baaa17aad 100644 --- a/frontend/src/components/projectDetail/index.js +++ b/frontend/src/components/projectDetail/index.js @@ -189,7 +189,7 @@ export const ProjectDetail = (props) => { /> From ef51caa317e2a9d2c97ed73a406f01a13feef6ba Mon Sep 17 00:00:00 2001 From: royallsilwallz Date: Tue, 8 Oct 2024 12:50:13 +0545 Subject: [PATCH 05/13] Show only `Instructions` tab when not logged in --- .../components/taskSelection/tabSelector.js | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) 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)} + > + +
+ ))} +
+ ); +}; From 5c4ef1b142a6370035fa7d1e2491c445bf8fb915 Mon Sep 17 00:00:00 2001 From: royallsilwallz Date: Tue, 8 Oct 2024 13:08:38 +0545 Subject: [PATCH 06/13] Navigate to login when `Map a Task` button is clicked without login --- frontend/src/components/taskSelection/footer.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/taskSelection/footer.js b/frontend/src/components/taskSelection/footer.js index 03e6113dfe..1cb9a7b2cd 100644 --- a/frontend/src/components/taskSelection/footer.js +++ b/frontend/src/components/taskSelection/footer.js @@ -221,7 +221,17 @@ const TaskSelectionFooter = ({
- diff --git a/frontend/src/components/taskSelection/index.js b/frontend/src/components/taskSelection/index.js index f35af3cccc..650f035604 100644 --- a/frontend/src/components/taskSelection/index.js +++ b/frontend/src/components/taskSelection/index.js @@ -135,9 +135,22 @@ export function TaskSelection({ project }: Object) { [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(() => { + // 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)) { @@ -147,7 +160,7 @@ export function TaskSelection({ project }: Object) { } isFirstRender.current = false; } - }, [contributions, user.username, user, textSearch, setActiveSection]); + }, [contributions, user.username, user, textSearch, setActiveSection, location, projectId]); useEffect(() => { // run it only when the component is initialized From cb58a096aa465d23d3286995e20ee92aaa5bd2cb Mon Sep 17 00:00:00 2001 From: royallsilwallz Date: Tue, 12 Nov 2024 13:24:04 +0545 Subject: [PATCH 13/13] Add `from` state to MemoryRouter in Tasks page test cases - Fix test case failure for redirect issue in task page --- frontend/src/components/taskSelection/tests/index.test.js | 4 +++- frontend/src/views/tests/taskSelection.test.js | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/taskSelection/tests/index.test.js b/frontend/src/components/taskSelection/tests/index.test.js index f81dfea791..e069d95ed2 100644 --- a/frontend/src/components/taskSelection/tests/index.test.js +++ b/frontend/src/components/taskSelection/tests/index.test.js @@ -15,7 +15,9 @@ describe('Contributions', () => { return { user: userEvent.setup(), ...render( - + { return { user: userEvent.setup(), ...render( - + { return { user: userEvent.setup(), ...render( - +