Skip to content

Commit

Permalink
Merge pull request #5953 from hotosm/enhancement/5435-click-mapped-task
Browse files Browse the repository at this point in the history
Enable ability for users to select tasks they mapped
  • Loading branch information
HelNershingThapa authored Sep 5, 2023
2 parents 45bf393 + f1f1acd commit df1dd6d
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 18 deletions.
12 changes: 11 additions & 1 deletion frontend/src/components/taskSelection/footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -178,6 +185,9 @@ const TaskSelectionFooter = ({ defaultUserEditor, project, tasks, taskAction, se
error={lockError}
close={close}
lockTasks={lockTasks}
tasks={tasks}
selectedTasks={selectedTasks}
setSelectedTasks={setSelectedTasks}
/>
)}
</Popup>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/taskSelection/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ export function TaskSelection({ project, type, loading }: Object) {
tasks={tasks}
taskAction={taskAction}
selectedTasks={curatedSelectedTasks}
setSelectedTasks={setSelectedTasks}
/>
</Suspense>
</ReactPlaceholder>
Expand Down
86 changes: 78 additions & 8 deletions frontend/src/components/taskSelection/lockedTasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<h3 className="barlow-condensed f3 fw6 mv0">
Expand All @@ -135,16 +137,77 @@ export function LockError({ error, close }) {
<FormattedMessage {...messages.lockErrorDescription} />
)}
</div>
<div className="w-100 pt3">
<Button onClick={() => close()} className="bg-red white mr2">
<FormattedMessage {...messages.closeModal} />
</Button>
</div>
<LockErrorButtons
close={close}
shouldShowDeselectButton={shouldShowDeselectButton}
tasks={tasks}
selectedTasks={selectedTasks}
setSelectedTasks={setSelectedTasks}
lockTasks={lockTasks}
/>
</>
);
}

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 (
<div className="w-100 pt3 flex justify-end">
<Button
onClick={close}
className={`mr2 ${shouldShowDeselectButton ? 'bg-transparent black' : 'bg-red white'}`}
>
<FormattedMessage {...messages.closeModal} />
</Button>
{shouldShowDeselectButton && (
<Button onClick={handleDeselectAndValidate} className="bg-red white mr2">
<FormattedMessage {...messages.deselectAndValidate} />
</Button>
)}
</div>
);
}

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;
Expand All @@ -155,7 +218,14 @@ export function LockedTaskModalContent({ project, error, close, lockTasks }: Obj
{/* Other error happened */}
{error === 'JOSM' && <LockError error={error} close={close} />}
{!lockedTasks.project && !licenseError && error !== 'JOSM' && (
<LockError error={error} close={close} />
<LockError
error={error}
close={close}
lockTasks={lockTasks}
tasks={tasks}
selectedTasks={selectedTasks}
setSelectedTasks={setSelectedTasks}
/>
)}
{/* User has tasks locked on another project */}
{lockedTasks.project && lockedTasks.project !== project.projectId && error !== 'JOSM' && (
Expand Down
8 changes: 2 additions & 6 deletions frontend/src/components/taskSelection/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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();
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/taskSelection/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
65 changes: 64 additions & 1 deletion frontend/src/components/taskSelection/tests/lockedTasks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -171,6 +171,69 @@ describe('License Modal', () => {
});
});

describe('LockError for CannotValidateMappedTask', () => {
it('should display the Deselect and continue button', () => {
render(
<ReduxIntlProviders>
<LockError error="CannotValidateMappedTask" selectedTasks={[1, 2, 3]} />
</ReduxIntlProviders>,
);
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(
<ReduxIntlProviders>
<LockError error="CannotValidateMappedTask" selectedTasks={[1]} />
</ReduxIntlProviders>,
);
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(
<ReduxIntlProviders>
<LockError
error="CannotValidateMappedTask"
selectedTasks={[1, 2]}
lockTasks={lockTasksFnMock}
setSelectedTasks={setSelectedTasksFnMock}
tasks={dummyTasks}
/>
</ReduxIntlProviders>,
);
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,
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit df1dd6d

Please sign in to comment.