Skip to content

Commit

Permalink
Merge pull request hotosm#6592 from hotosm/feature/6302-view-task-ins…
Browse files Browse the repository at this point in the history
…tructions

View Task Instructions without login
  • Loading branch information
ramyaragupathy authored Nov 13, 2024
2 parents f2ab8ad + cb58a09 commit ac37153
Show file tree
Hide file tree
Showing 16 changed files with 271 additions and 122 deletions.
3 changes: 2 additions & 1 deletion frontend/src/api/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand All @@ -51,6 +51,7 @@ export const useProjectQuery = (projectId) => {
return useQuery({
queryKey: ['project', projectId],
queryFn: fetchProject,
...otherOptions,
});
};
export const useProjectSummaryQuery = (projectId, otherOptions = {}) => {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/footer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/components/projectDetail/footer.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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 (
<div
Expand Down Expand Up @@ -92,7 +93,12 @@ export const ProjectDetailFooter = ({ className, projectId }) => {
<div className="flex items-center ml-auto gap-1">
<ShareButton projectId={projectId} />
{userIsloggedIn && <AddToFavorites projectId={projectId} />}
<Link to={`./tasks`} className="">
<Link
to={`./tasks`}
// add previous path to history location to track
// if the user is from project detail page
state={{ from: pathname }}
>
<Button className="white bg-red h3 w5">
<FormattedMessage {...messages.contribute} />
</Button>
Expand Down
19 changes: 16 additions & 3 deletions frontend/src/components/projectDetail/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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: [
Expand Down Expand Up @@ -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,
);
Expand Down Expand Up @@ -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}
/>
<a
href={`/projects/${projectId}/instructions`}
className="ph4 ttu db f5 blue-dark fw6 project-instructions-link"
>
<FormattedMessage {...messages.viewProjectSpecificInstructions} />
</a>
<a href="#coordination" style={{ visibility: 'hidden' }} name="coordination">
<FormattedMessage {...messages.coordination} />
</a>
Expand Down Expand Up @@ -435,6 +447,7 @@ ProjectDetailMap.propTypes = {
type: PropTypes.string,
tasksError: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
projectLoading: PropTypes.bool,
taskBordersOnly: PropTypes.bool,
};

ProjectDetailLeft.propTypes = {
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/projectDetail/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
});
4 changes: 4 additions & 0 deletions frontend/src/components/projectDetail/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@
.react-tooltip#dueDateBoxTooltip {
z-index: 999;
}

.project-instructions-link {
letter-spacing: -0.0857513px;
}
18 changes: 16 additions & 2 deletions frontend/src/components/taskSelection/footer.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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);
Expand Down Expand Up @@ -221,7 +222,20 @@ const TaskSelectionFooter = ({
</div>
<div className="w-30-ns w-60 fl tr">
<div className="mt3">
<Button className="white bg-red fw5" onClick={() => lockTasks()} loading={isPending}>
<Button
className="white bg-red fw5"
onClick={() => {
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' ? (
Expand Down
97 changes: 70 additions & 27 deletions frontend/src/components/taskSelection/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -31,6 +32,7 @@ import {
useTasksQuery,
} from '../../api/projects';
import { useTeamsQuery } from '../../api/teams';

const TaskSelectionFooter = lazy(() => import('./footer'));

const getRandomTaskByAction = (activities, taskAction) => {
Expand All @@ -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(
{
Expand All @@ -74,6 +79,7 @@ export function TaskSelection({ project }: Object) {
},
{
useErrorBoundary: true,
enabled: !!token,
},
);
const { data: activities, refetch: getActivities } = useActivitiesQuery(projectId);
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -291,6 +321,7 @@ export function TaskSelection({ project }: Object) {
<ChangesetCommentTags tags={project.changesetComment} />
</>
) : null}

{activeSection === 'contributions' ? (
<Contributions
project={project}
Expand All @@ -306,28 +337,40 @@ export function TaskSelection({ project }: Object) {
</div>
</div>
<div className="w-100 w-50-ns fl h-100 relative">
<ReactPlaceholder
showLoadingAnimation={true}
type={'media'}
rows={26}
delay={200}
ready={typeof tasks === 'object' && mapInit && !isPriorityAreasLoading}
>
<TasksMap
mapResults={tasks}
projectId={project.projectId}
error={typeof project !== 'object'}
loading={typeof project !== 'object'}
className="dib w-100 fl h-100-ns vh-75"
zoomedTaskId={zoomedTaskId}
selectTask={selectTask}
selected={selected}
{!token ? (
<ProjectDetailMap
project={project}
projectLoading={false}
tasksError={false}
tasks={project.tasks}
navigate={navigate}
type="detail"
taskBordersOnly={false}
priorityAreas={priorityAreas}
animateZoom={false}
/>
<TasksMapLegend />
</ReactPlaceholder>
) : (
<ReactPlaceholder
showLoadingAnimation={true}
type={'media'}
rows={26}
delay={200}
ready={typeof tasks === 'object' && mapInit && !isPriorityAreasLoading}
>
<TasksMap
mapResults={tasks}
projectId={project.projectId}
error={typeof project !== 'object'}
loading={typeof project !== 'object'}
className="dib w-100 fl h-100-ns vh-75"
zoomedTaskId={zoomedTaskId}
selectTask={selectTask}
selected={selected}
taskBordersOnly={false}
priorityAreas={priorityAreas}
animateZoom={false}
/>
<TasksMapLegend />
</ReactPlaceholder>
)}
</div>
</div>
<div className="cf w-100 bt b--grey-light fixed bottom-0 left-0 z-4">
Expand Down
36 changes: 21 additions & 15 deletions frontend/src/components/taskSelection/tabSelector.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';

import messages from './messages';

export const TabSelector = ({ activeSection, setActiveSection }) => (
<div className="ttu barlow-condensed f4 blue-dark bb b--grey-light">
{['tasks', 'instructions', 'contributions'].map((section) => (
<div
key={section}
role="button"
className={`mr4 pb2 fw5 pointer dib ${activeSection === section && 'bb bw1'}`}
style={{ letterSpacing: '-0.0857513px', borderColor: '#979797' }}
onClick={() => setActiveSection(section)}
>
<FormattedMessage {...messages[section]} />
</div>
))}
</div>
);
export const TabSelector = ({ activeSection, setActiveSection }) => {
const token = useSelector((state) => state.auth.token);
const tabs = token ? ['tasks', 'instructions', 'contributions'] : ['instructions'];

return (
<div className="ttu barlow-condensed f4 blue-dark bb b--grey-light">
{tabs.map((section) => (
<div
key={section}
role="button"
className={`mr4 pb2 fw5 pointer dib ${activeSection === section && 'bb bw1'}`}
style={{ letterSpacing: '-0.0857513px', borderColor: '#979797' }}
onClick={() => setActiveSection(section)}
>
<FormattedMessage {...messages[section]} />
</div>
))}
</div>
);
};
1 change: 1 addition & 0 deletions frontend/src/components/taskSelection/tests/footer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Loading

0 comments on commit ac37153

Please sign in to comment.