Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable ability for users to select tasks they mapped #5953

Merged
merged 2 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading