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",