Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ For more information about each release including git tags and artifacts, see [R

### Fixed

- Existing manual review queues can now be converted to or from an appeals queue from the queue edit form ([#1176](https://github.com/roostorg/coop/pull/1176) by [@reitblatt](https://github.com/reitblatt))
- Rule history dropping other rules' versions when filtered by start date ([#1056](https://github.com/roostorg/coop/pull/1056) by [@juanmrad](https://github.com/juanmrad))
- `RetryFailedNcmecDecisionsJob` ignoring `NCMEC_ENV` and retrying test decisions ([#928](https://github.com/roostorg/coop/pull/928) by [@taobojlen](https://github.com/taobojlen))
- Queue creation failing with "name already exists" on the default reviewer selection ([#1069](https://github.com/roostorg/coop/pull/1069) by [@jess-upscrolled](https://github.com/jess-upscrolled), closes [#1074](https://github.com/roostorg/coop/issues/1074))
Expand Down
34 changes: 33 additions & 1 deletion client/src/graphql/generated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

192 changes: 192 additions & 0 deletions client/src/webpages/dashboard/mrt/ManualReviewQueueForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { TooltipProvider } from '@/coop-ui/Tooltip';
import { MockedProvider, type MockedResponse } from '@apollo/client/testing';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { HelmetProvider } from 'react-helmet-async';
import { MemoryRouter, Route, Routes } from 'react-router-dom';

import '@testing-library/jest-dom/extend-expect';

import {
GQLManualReviewQueueDocument,
GQLQueueFormDataDocument,
GQLUpdateManualReviewQueueDocument,
type GQLUpdateManualReviewQueueMutationVariables,
} from '@/graphql/generated';

import ManualReviewQueueForm from './ManualReviewQueueForm';

const QUEUE_ID = 'queue-1';

function queueFormDataMock(hasAppealsEnabled: boolean): MockedResponse {
return {
request: { query: GQLQueueFormDataDocument },
maxUsageCount: Infinity,
result: {
data: {
myOrg: {
__typename: 'Org',
hasAppealsEnabled,
hasPartialItemsEndpoint: false,
users: [],
actions: [],
usersWhoCanReviewEveryQueue: [],
},
},
},
};
}

function manualReviewQueueMock(isAppealsQueue: boolean): MockedResponse {
return {
request: {
query: GQLManualReviewQueueDocument,
variables: { id: QUEUE_ID },
},
maxUsageCount: Infinity,
result: {
data: {
manualReviewQueue: {
__typename: 'ManualReviewQueue',
id: QUEUE_ID,
name: 'Existing Queue',
description: null,
explicitlyAssignedReviewers: [],
hiddenActionIds: [],
isAppealsQueue,
autoCloseJobs: false,
clearReportsDisposition: null,
clearReportsScope: 'CURRENT_QUEUE',
clearReportsTriggerActionIds: [],
},
},
},
};
}

function renderEditForm(mocks: MockedResponse[]) {
return render(
<HelmetProvider>
<TooltipProvider>
<MockedProvider mocks={mocks}>
<MemoryRouter
initialEntries={[
`/dashboard/manual_review/queues/form/${QUEUE_ID}`,
]}
>
<Routes>
<Route
path="/dashboard/manual_review/queues/form/:id"
element={<ManualReviewQueueForm />}
/>
</Routes>
</MemoryRouter>
</MockedProvider>
</TooltipProvider>
</HelmetProvider>,
);
}

const appealsCheckbox = () =>
screen.queryByRole('checkbox', { name: /this is an appeals queue/i });

describe('ManualReviewQueueForm (edit)', () => {
it('shows the appeals queue checkbox when appeals are enabled', async () => {
renderEditForm([queueFormDataMock(true), manualReviewQueueMock(false)]);

await screen.findByText('Update Manual Review Queue');
await waitFor(() => expect(appealsCheckbox()).toBeInTheDocument());
expect(appealsCheckbox()).not.toBeChecked();
expect(
screen.getByText(/can only be converted to or from an appeals queue/i),
).toBeInTheDocument();
});

it('reflects the queue being an appeals queue already', async () => {
renderEditForm([queueFormDataMock(true), manualReviewQueueMock(true)]);

await screen.findByText('Update Manual Review Queue');
await waitFor(() => expect(appealsCheckbox()).toBeChecked());
});

it('hides the appeals queue checkbox when appeals are disabled', async () => {
renderEditForm([queueFormDataMock(false), manualReviewQueueMock(false)]);

await screen.findByText('Update Manual Review Queue');
expect(appealsCheckbox()).not.toBeInTheDocument();
});

it('sends the new appeals flag when saving', async () => {
let calledVariables:
GQLUpdateManualReviewQueueMutationVariables | undefined;
const updateMock: MockedResponse = {
request: { query: GQLUpdateManualReviewQueueDocument },
variableMatcher: (variables) => {
calledVariables =
variables as GQLUpdateManualReviewQueueMutationVariables;
return true;
},
result: {
data: {
updateManualReviewQueue: {
__typename: 'MutateManualReviewQueueSuccessResponse',
data: {
__typename: 'ManualReviewQueue',
id: QUEUE_ID,
name: 'Existing Queue',
description: null,
},
},
},
},
};
renderEditForm([
queueFormDataMock(true),
manualReviewQueueMock(false),
updateMock,
]);

await screen.findByText('Update Manual Review Queue');
await waitFor(() => expect(appealsCheckbox()).toBeInTheDocument());
fireEvent.click(appealsCheckbox()!);
await waitFor(() => expect(appealsCheckbox()).toBeChecked());
fireEvent.click(screen.getByRole('button', { name: /save changes/i }));

await waitFor(() => expect(calledVariables).toBeDefined());
expect(calledVariables?.input).toMatchObject({
id: QUEUE_ID,
isAppealsQueue: true,
});
expect(await screen.findByText('Changes Saved')).toBeInTheDocument();
});

it('surfaces the server explanation when the queue cannot be converted', async () => {
const title =
'This queue cannot be converted while it still has pending jobs. Empty the queue first.';
const updateMock: MockedResponse = {
request: { query: GQLUpdateManualReviewQueueDocument },
variableMatcher: () => true,
result: {
data: {
updateManualReviewQueue: {
__typename: 'UnableToChangeQueueTypeError',
title,
status: 409,
type: ['/errors/conflict'],
},
},
},
};
renderEditForm([
queueFormDataMock(true),
manualReviewQueueMock(false),
updateMock,
]);

await screen.findByText('Update Manual Review Queue');
await waitFor(() => expect(appealsCheckbox()).toBeInTheDocument());
fireEvent.click(appealsCheckbox()!);
fireEvent.click(screen.getByRole('button', { name: /save changes/i }));

expect(await screen.findByText(title)).toBeInTheDocument();
});
});
23 changes: 22 additions & 1 deletion client/src/webpages/dashboard/mrt/ManualReviewQueueForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ gql`
status
type
}
... on UnableToChangeQueueTypeError {
title
status
type
}
... on NotFoundError {
title
status
Expand Down Expand Up @@ -215,6 +220,13 @@ export default function ManualReviewQueueForm() {
buttonText: 'OK',
});
break;
case 'UnableToChangeQueueTypeError':
setModalInfo({
title: 'Error Saving Changes',
body: response.updateManualReviewQueue.title,
buttonText: 'OK',
});
break;
case 'MutateManualReviewQueueSuccessResponse':
setModalInfo({
title: 'Changes Saved',
Expand Down Expand Up @@ -388,6 +400,7 @@ export default function ManualReviewQueueForm() {
hiddenActionIds,
),
autoCloseJobs,
isAppealsQueue,
clearReportsDisposition,
clearReportsScope,
clearReportsTriggerActionIds:
Expand All @@ -405,6 +418,7 @@ export default function ManualReviewQueueForm() {
hiddenActionIds,
id,
initiallyHiddenActionIds,
isAppealsQueue,
moderatorsWithAccess,
queueDescription,
queueName,
Expand Down Expand Up @@ -633,7 +647,7 @@ export default function ManualReviewQueueForm() {
)}
</div>
)}
{isCreateForm && data?.myOrg?.hasAppealsEnabled ? (
{data?.myOrg?.hasAppealsEnabled ? (
<div className="mt-8">
<div className="flex items-center space-x-2">
<Checkbox
Expand All @@ -643,6 +657,13 @@ export default function ManualReviewQueueForm() {
/>
<Label htmlFor="is-appeals-queue">This is an Appeals Queue</Label>
</div>
{!isCreateForm && (
<div className="mt-2 text-slate-500">
An existing queue can only be converted to or from an appeals
queue while it has no pending jobs and no routing rules point to
it. The default queue cannot be converted.
</div>
)}
</div>
) : null}
{divider()}
Expand Down
Loading
Loading