Skip to content

Commit f1f1acd

Browse files
Add "Deselect and validate" button in lock error modal
- Enhances the `LockError` modal by introducing a "Deselect and Continue" button. The button is displayed when a `CannotValidateMappedTask` task lock error occurs. The "Close" button is shown with the primary variant when only a single task is selected. - Conditionally displayed the buttons based on the user's selected tasks and mapping status. - Separated the action buttons into a new component named `LockErrorButtons`. - Moved the logic for handling task deselection and validation into the `LockErrorButtons` component. - Updated the existing test cases to reflect the new changes made in the `LockErrorButtons` component.
1 parent aa9460f commit f1f1acd

File tree

6 files changed

+159
-10
lines changed

6 files changed

+159
-10
lines changed

frontend/src/components/taskSelection/footer.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ import { Imagery } from './imagery';
1515
import { MappingTypes } from '../mappingTypes';
1616
import { LockedTaskModalContent } from './lockedTasks';
1717

18-
const TaskSelectionFooter = ({ defaultUserEditor, project, tasks, taskAction, selectedTasks }) => {
18+
const TaskSelectionFooter = ({
19+
defaultUserEditor,
20+
project,
21+
tasks,
22+
taskAction,
23+
selectedTasks,
24+
setSelectedTasks,
25+
}) => {
1926
const navigate = useNavigate();
2027
const token = useSelector((state) => state.auth.token);
2128
const locale = useSelector((state) => state.preferences.locale);
@@ -178,6 +185,9 @@ const TaskSelectionFooter = ({ defaultUserEditor, project, tasks, taskAction, se
178185
error={lockError}
179186
close={close}
180187
lockTasks={lockTasks}
188+
tasks={tasks}
189+
selectedTasks={selectedTasks}
190+
setSelectedTasks={setSelectedTasks}
181191
/>
182192
)}
183193
</Popup>

frontend/src/components/taskSelection/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ export function TaskSelection({ project, type, loading }: Object) {
408408
tasks={tasks}
409409
taskAction={taskAction}
410410
selectedTasks={curatedSelectedTasks}
411+
setSelectedTasks={setSelectedTasks}
411412
/>
412413
</Suspense>
413414
</ReactPlaceholder>

frontend/src/components/taskSelection/lockedTasks.js

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ export const LicenseError = ({ id, close, lockTasks }) => {
118118
);
119119
};
120120

121-
export function LockError({ error, close }) {
121+
export function LockError({ error, close, tasks, selectedTasks, setSelectedTasks, lockTasks }) {
122+
const shouldShowDeselectButton = error === 'CannotValidateMappedTask' && selectedTasks.length > 1;
123+
122124
return (
123125
<>
124126
<h3 className="barlow-condensed f3 fw6 mv0">
@@ -135,16 +137,77 @@ export function LockError({ error, close }) {
135137
<FormattedMessage {...messages.lockErrorDescription} />
136138
)}
137139
</div>
138-
<div className="w-100 pt3">
139-
<Button onClick={() => close()} className="bg-red white mr2">
140-
<FormattedMessage {...messages.closeModal} />
141-
</Button>
142-
</div>
140+
<LockErrorButtons
141+
close={close}
142+
shouldShowDeselectButton={shouldShowDeselectButton}
143+
tasks={tasks}
144+
selectedTasks={selectedTasks}
145+
setSelectedTasks={setSelectedTasks}
146+
lockTasks={lockTasks}
147+
/>
143148
</>
144149
);
145150
}
146151

147-
export function LockedTaskModalContent({ project, error, close, lockTasks }: Object) {
152+
function LockErrorButtons({
153+
close,
154+
shouldShowDeselectButton,
155+
lockTasks,
156+
tasks,
157+
selectedTasks,
158+
setSelectedTasks,
159+
}) {
160+
const user = useSelector((state) => state.auth.userDetails);
161+
const [hasTasksChanged, setHasTasksChanged] = useState(false);
162+
163+
const handleDeselectAndValidate = () => {
164+
const userMappedTaskIds = tasks.features
165+
.filter((feature) => feature.properties.mappedBy === user.id)
166+
.map((feature) => feature.properties.taskId);
167+
168+
const remainingSelectedTasks = selectedTasks.filter(
169+
(selectedTask) => !userMappedTaskIds.includes(selectedTask),
170+
);
171+
setSelectedTasks(remainingSelectedTasks);
172+
// Set the flag to indicate that tasks have changed.
173+
// Note: The introduction of useEffect pattern might benefit
174+
// from future optimization.
175+
setHasTasksChanged(true);
176+
};
177+
178+
useEffect(() => {
179+
if (hasTasksChanged) {
180+
lockTasks();
181+
setHasTasksChanged(false);
182+
}
183+
}, [hasTasksChanged, lockTasks]);
184+
185+
return (
186+
<div className="w-100 pt3 flex justify-end">
187+
<Button
188+
onClick={close}
189+
className={`mr2 ${shouldShowDeselectButton ? 'bg-transparent black' : 'bg-red white'}`}
190+
>
191+
<FormattedMessage {...messages.closeModal} />
192+
</Button>
193+
{shouldShowDeselectButton && (
194+
<Button onClick={handleDeselectAndValidate} className="bg-red white mr2">
195+
<FormattedMessage {...messages.deselectAndValidate} />
196+
</Button>
197+
)}
198+
</div>
199+
);
200+
}
201+
202+
export function LockedTaskModalContent({
203+
project,
204+
error,
205+
close,
206+
lockTasks,
207+
tasks,
208+
selectedTasks,
209+
setSelectedTasks,
210+
}: Object) {
148211
const lockedTasks = useGetLockedTasks();
149212
const action = lockedTasks.status === 'LOCKED_FOR_VALIDATION' ? 'validate' : 'map';
150213
const licenseError = error === 'UserLicenseError' && !lockedTasks.project;
@@ -155,7 +218,14 @@ export function LockedTaskModalContent({ project, error, close, lockTasks }: Obj
155218
{/* Other error happened */}
156219
{error === 'JOSM' && <LockError error={error} close={close} />}
157220
{!lockedTasks.project && !licenseError && error !== 'JOSM' && (
158-
<LockError error={error} close={close} />
221+
<LockError
222+
error={error}
223+
close={close}
224+
lockTasks={lockTasks}
225+
tasks={tasks}
226+
selectedTasks={selectedTasks}
227+
setSelectedTasks={setSelectedTasks}
228+
/>
159229
)}
160230
{/* User has tasks locked on another project */}
161231
{lockedTasks.project && lockedTasks.project !== project.projectId && error !== 'JOSM' && (

frontend/src/components/taskSelection/messages.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export default defineMessages({
2828
id: 'project.tasks.unsaved_map_changes.actions.close_modal',
2929
defaultMessage: 'Close',
3030
},
31+
deselectAndValidate: {
32+
id: 'project.tasks.validation.cannot_validate_mapped_tasks.deselect_and_validate',
33+
defaultMessage: 'Deselect and validate',
34+
},
3135
cantValidateMappedTask: {
3236
id: 'project.tasks.select.cantValidateMappedTask',
3337
defaultMessage: 'This task was mapped by you',

frontend/src/components/taskSelection/tests/lockedTasks.test.js

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33
import TestRenderer from 'react-test-renderer';
44
import { FormattedMessage } from 'react-intl';
55
import { MemoryRouter } from 'react-router-dom';
6-
import { render, screen, waitFor } from '@testing-library/react';
6+
import { act, render, screen, waitFor } from '@testing-library/react';
77
import userEvent from '@testing-library/user-event';
88

99
import {
@@ -171,6 +171,69 @@ describe('License Modal', () => {
171171
});
172172
});
173173

174+
describe('LockError for CannotValidateMappedTask', () => {
175+
it('should display the Deselect and continue button', () => {
176+
render(
177+
<ReduxIntlProviders>
178+
<LockError error="CannotValidateMappedTask" selectedTasks={[1, 2, 3]} />
179+
</ReduxIntlProviders>,
180+
);
181+
expect(screen.getByRole('button', { name: 'Deselect and validate' })).toBeInTheDocument();
182+
});
183+
184+
it('should not display the Deselect and continue button if only one task is selected for validation', () => {
185+
render(
186+
<ReduxIntlProviders>
187+
<LockError error="CannotValidateMappedTask" selectedTasks={[1]} />
188+
</ReduxIntlProviders>,
189+
);
190+
expect(screen.queryByRole('button', { name: 'Deselect and validate' })).not.toBeInTheDocument();
191+
});
192+
193+
it('should lock tasks after deselecting the tasks that the user mapped from the list of selected tasks', async () => {
194+
const lockTasksFnMock = jest.fn();
195+
const setSelectedTasksFnMock = jest.fn();
196+
const dummyTasks = {
197+
features: [
198+
{
199+
properties: {
200+
taskId: 1,
201+
mappedBy: 123, // Same value as the logged in user's username
202+
},
203+
},
204+
{
205+
properties: {
206+
taskId: 2,
207+
mappedBy: 321,
208+
},
209+
},
210+
],
211+
};
212+
213+
act(() => {
214+
store.dispatch({
215+
type: 'SET_USER_DETAILS',
216+
userDetails: { id: 123 },
217+
});
218+
});
219+
220+
render(
221+
<ReduxIntlProviders>
222+
<LockError
223+
error="CannotValidateMappedTask"
224+
selectedTasks={[1, 2]}
225+
lockTasks={lockTasksFnMock}
226+
setSelectedTasks={setSelectedTasksFnMock}
227+
tasks={dummyTasks}
228+
/>
229+
</ReduxIntlProviders>,
230+
);
231+
const user = userEvent.setup();
232+
await user.click(screen.queryByRole('button', { name: 'Deselect and validate' }));
233+
expect(lockTasksFnMock).toHaveBeenCalledTimes(1);
234+
});
235+
});
236+
174237
test('SameProjectLock should display relevant message when user has multiple tasks locked', async () => {
175238
const lockedTasksSample = {
176239
project: 5871,

frontend/src/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,7 @@
623623
"project.tasks.unsaved_map_changes.reload_editor": "Save or undo it to be able to switch editors",
624624
"project.tasks.unsaved_map_changes.tooltip": "You have unsaved edits. Save or undo them to submit this task.",
625625
"project.tasks.unsaved_map_changes.actions.close_modal": "Close",
626+
"project.tasks.validation.cannot_validate_mapped_tasks.deselect_and_validate": "Deselect and validate",
626627
"project.tasks.select.cantValidateMappedTask": "This task was mapped by you",
627628
"project.tasks.no_mapped_tasks_selected": "No mapped tasks selected",
628629
"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.",

0 commit comments

Comments
 (0)