diff --git a/frontend/src/components/taskSelection/footer.js b/frontend/src/components/taskSelection/footer.js
index bcedf2bdad..c26acff7c9 100644
--- a/frontend/src/components/taskSelection/footer.js
+++ b/frontend/src/components/taskSelection/footer.js
@@ -15,7 +15,14 @@ import { Imagery } from './imagery';
import { MappingTypes } from '../mappingTypes';
import { LockedTaskModalContent } from './lockedTasks';
-const TaskSelectionFooter = ({ defaultUserEditor, project, tasks, taskAction, selectedTasks }) => {
+const TaskSelectionFooter = ({
+ defaultUserEditor,
+ project,
+ tasks,
+ taskAction,
+ selectedTasks,
+ setSelectedTasks,
+}) => {
const navigate = useNavigate();
const token = useSelector((state) => state.auth.token);
const locale = useSelector((state) => state.preferences.locale);
@@ -178,6 +185,9 @@ const TaskSelectionFooter = ({ defaultUserEditor, project, tasks, taskAction, se
error={lockError}
close={close}
lockTasks={lockTasks}
+ tasks={tasks}
+ selectedTasks={selectedTasks}
+ setSelectedTasks={setSelectedTasks}
/>
)}
diff --git a/frontend/src/components/taskSelection/index.js b/frontend/src/components/taskSelection/index.js
index f8454ea9e1..dd2b89c6fc 100644
--- a/frontend/src/components/taskSelection/index.js
+++ b/frontend/src/components/taskSelection/index.js
@@ -408,6 +408,7 @@ export function TaskSelection({ project, type, loading }: Object) {
tasks={tasks}
taskAction={taskAction}
selectedTasks={curatedSelectedTasks}
+ setSelectedTasks={setSelectedTasks}
/>
diff --git a/frontend/src/components/taskSelection/lockedTasks.js b/frontend/src/components/taskSelection/lockedTasks.js
index a141873de6..461d420e9b 100644
--- a/frontend/src/components/taskSelection/lockedTasks.js
+++ b/frontend/src/components/taskSelection/lockedTasks.js
@@ -118,7 +118,9 @@ export const LicenseError = ({ id, close, lockTasks }) => {
);
};
-export function LockError({ error, close }) {
+export function LockError({ error, close, tasks, selectedTasks, setSelectedTasks, lockTasks }) {
+ const shouldShowDeselectButton = error === 'CannotValidateMappedTask' && selectedTasks.length > 1;
+
return (
<>
@@ -135,16 +137,77 @@ export function LockError({ error, close }) {
)}
-
-
-
+
>
);
}
-export function LockedTaskModalContent({ project, error, close, lockTasks }: Object) {
+function LockErrorButtons({
+ close,
+ shouldShowDeselectButton,
+ lockTasks,
+ tasks,
+ selectedTasks,
+ setSelectedTasks,
+}) {
+ const user = useSelector((state) => state.auth.userDetails);
+ const [hasTasksChanged, setHasTasksChanged] = useState(false);
+
+ const handleDeselectAndValidate = () => {
+ const userMappedTaskIds = tasks.features
+ .filter((feature) => feature.properties.mappedBy === user.id)
+ .map((feature) => feature.properties.taskId);
+
+ const remainingSelectedTasks = selectedTasks.filter(
+ (selectedTask) => !userMappedTaskIds.includes(selectedTask),
+ );
+ setSelectedTasks(remainingSelectedTasks);
+ // Set the flag to indicate that tasks have changed.
+ // Note: The introduction of useEffect pattern might benefit
+ // from future optimization.
+ setHasTasksChanged(true);
+ };
+
+ useEffect(() => {
+ if (hasTasksChanged) {
+ lockTasks();
+ setHasTasksChanged(false);
+ }
+ }, [hasTasksChanged, lockTasks]);
+
+ return (
+
+
+ {shouldShowDeselectButton && (
+
+ )}
+
+ );
+}
+
+export function LockedTaskModalContent({
+ project,
+ error,
+ close,
+ lockTasks,
+ tasks,
+ selectedTasks,
+ setSelectedTasks,
+}: Object) {
const lockedTasks = useGetLockedTasks();
const action = lockedTasks.status === 'LOCKED_FOR_VALIDATION' ? 'validate' : 'map';
const licenseError = error === 'UserLicenseError' && !lockedTasks.project;
@@ -155,7 +218,14 @@ export function LockedTaskModalContent({ project, error, close, lockTasks }: Obj
{/* Other error happened */}
{error === 'JOSM' && }
{!lockedTasks.project && !licenseError && error !== 'JOSM' && (
-
+
)}
{/* User has tasks locked on another project */}
{lockedTasks.project && lockedTasks.project !== project.projectId && error !== 'JOSM' && (
diff --git a/frontend/src/components/taskSelection/map.js b/frontend/src/components/taskSelection/map.js
index d2114ddc6f..a3eccd7fba 100644
--- a/frontend/src/components/taskSelection/map.js
+++ b/frontend/src/components/taskSelection/map.js
@@ -83,11 +83,8 @@ export const TasksMap = ({
useLayoutEffect(() => {
const onSelectTaskClick = (e) => {
- const { mappedBy, taskStatus } = e.features[0].properties;
- const task = e.features && e.features[0].properties;
- if (!(mappedBy === authDetails.id && taskStatus === 'MAPPED')) {
- selectTask && selectTask(task.taskId, task.taskStatus);
- }
+ const task = e.features?.[0].properties;
+ selectTask?.(task.taskId, task.taskStatus);
};
const countryMapLayers = [
@@ -374,7 +371,6 @@ export const TasksMap = ({
e.features[0].properties.taskStatus === 'MAPPED'
) {
popup.addTo(map);
- map.getCanvas().style.cursor = 'not-allowed';
} else {
map.getCanvas().style.cursor = 'pointer';
popup.isOpen() && popup.remove();
diff --git a/frontend/src/components/taskSelection/messages.js b/frontend/src/components/taskSelection/messages.js
index a24643b2dc..b73b29452a 100644
--- a/frontend/src/components/taskSelection/messages.js
+++ b/frontend/src/components/taskSelection/messages.js
@@ -28,9 +28,13 @@ export default defineMessages({
id: 'project.tasks.unsaved_map_changes.actions.close_modal',
defaultMessage: 'Close',
},
+ deselectAndValidate: {
+ id: 'project.tasks.validation.cannot_validate_mapped_tasks.deselect_and_validate',
+ defaultMessage: 'Deselect and validate',
+ },
cantValidateMappedTask: {
id: 'project.tasks.select.cantValidateMappedTask',
- defaultMessage: 'You cannot validate tasks that you mapped',
+ defaultMessage: 'This task was mapped by you',
},
noMappedTasksSelectedError: {
id: 'project.tasks.no_mapped_tasks_selected',
diff --git a/frontend/src/components/taskSelection/tests/lockedTasks.test.js b/frontend/src/components/taskSelection/tests/lockedTasks.test.js
index b623a9bdcc..7b10f43afc 100644
--- a/frontend/src/components/taskSelection/tests/lockedTasks.test.js
+++ b/frontend/src/components/taskSelection/tests/lockedTasks.test.js
@@ -3,7 +3,7 @@ import React from 'react';
import TestRenderer from 'react-test-renderer';
import { FormattedMessage } from 'react-intl';
import { MemoryRouter } from 'react-router-dom';
-import { render, screen, waitFor } from '@testing-library/react';
+import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
@@ -171,6 +171,69 @@ describe('License Modal', () => {
});
});
+describe('LockError for CannotValidateMappedTask', () => {
+ it('should display the Deselect and continue button', () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByRole('button', { name: 'Deselect and validate' })).toBeInTheDocument();
+ });
+
+ it('should not display the Deselect and continue button if only one task is selected for validation', () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.queryByRole('button', { name: 'Deselect and validate' })).not.toBeInTheDocument();
+ });
+
+ it('should lock tasks after deselecting the tasks that the user mapped from the list of selected tasks', async () => {
+ const lockTasksFnMock = jest.fn();
+ const setSelectedTasksFnMock = jest.fn();
+ const dummyTasks = {
+ features: [
+ {
+ properties: {
+ taskId: 1,
+ mappedBy: 123, // Same value as the logged in user's username
+ },
+ },
+ {
+ properties: {
+ taskId: 2,
+ mappedBy: 321,
+ },
+ },
+ ],
+ };
+
+ act(() => {
+ store.dispatch({
+ type: 'SET_USER_DETAILS',
+ userDetails: { id: 123 },
+ });
+ });
+
+ render(
+
+
+ ,
+ );
+ const user = userEvent.setup();
+ await user.click(screen.queryByRole('button', { name: 'Deselect and validate' }));
+ expect(lockTasksFnMock).toHaveBeenCalledTimes(1);
+ });
+});
+
test('SameProjectLock should display relevant message when user has multiple tasks locked', async () => {
const lockedTasksSample = {
project: 5871,
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
index 9901b9ba1d..e74401293b 100644
--- a/frontend/src/locales/en.json
+++ b/frontend/src/locales/en.json
@@ -623,7 +623,8 @@
"project.tasks.unsaved_map_changes.reload_editor": "Save or undo it to be able to switch editors",
"project.tasks.unsaved_map_changes.tooltip": "You have unsaved edits. Save or undo them to submit this task.",
"project.tasks.unsaved_map_changes.actions.close_modal": "Close",
- "project.tasks.select.cantValidateMappedTask": "You cannot validate tasks that you mapped",
+ "project.tasks.validation.cannot_validate_mapped_tasks.deselect_and_validate": "Deselect and validate",
+ "project.tasks.select.cantValidateMappedTask": "This task was mapped by you",
"project.tasks.no_mapped_tasks_selected": "No mapped tasks selected",
"project.tasks.no_mapped_tasks_selected.description": "It was not possible to lock the selected tasks, as none of them are on the mapped status.",
"project.tasks.invalid_task_state_errortitle": "Invalid Task State",