diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/MergedReportsComponent.test.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/MergedReportsComponent.test.tsx index e98ffca67..702a6dadb 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/MergedReportsComponent.test.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/MergedReportsComponent.test.tsx @@ -1,5 +1,5 @@ import { MockedProvider } from '@apollo/client/testing'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen, within } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; @@ -103,3 +103,119 @@ describe('MergedReportsComponent invalidation actions', () => { ).not.toBeInTheDocument(); }); }); + +const sortableReportHistory = [ + { + reportId: 'sort_primary', + reportedAt: '2026-03-01T12:00:00Z', + policyId: null, + reason: 'Primary', + reporterId: null, + }, + { + reportId: 'sort_zeta', + reportedAt: '2026-01-02T00:00:00Z', + policyId: 'policy_zeta', + reason: 'Bravo', + reporterId: { id: 'charlie', typeId: 'user_type' }, + }, + { + reportId: 'sort_beta', + reportedAt: '2025-12-30T00:00:00Z', + policyId: 'policy_beta', + reason: 'Charlie', + reporterId: { id: 'alpha', typeId: 'user_type' }, + }, + { + reportId: 'sort_alpha', + reportedAt: '2026-02-01T12:00:00Z', + policyId: 'policy_alpha', + reason: 'Alpha', + reporterId: { id: 'bravo', typeId: 'user_type' }, + }, +]; + +const sortingMocks = [ + { + request: { + query: GQLGetUserItemsDocument, + variables: { + itemIdentifiers: [ + { id: 'charlie', typeId: 'user_type' }, + { id: 'alpha', typeId: 'user_type' }, + { id: 'bravo', typeId: 'user_type' }, + ], + }, + }, + result: { data: { latestItemSubmissions: [] } }, + }, + { + request: { query: GQLPoliciesDocument }, + result: { + data: { + myOrg: { + id: 'org', + __typename: 'Org', + policies: [ + { id: 'policy_zeta', name: 'Zeta Policy', __typename: 'Policy' }, + { id: 'policy_beta', name: 'Beta Policy', __typename: 'Policy' }, + { id: 'policy_alpha', name: 'Alpha Policy', __typename: 'Policy' }, + ], + }, + }, + }, + }, +]; + +function renderedReasons() { + return within(screen.getAllByRole('rowgroup')[1]) + .getAllByRole('row') + .map((row) => within(row).getAllByRole('cell')[2].textContent); +} + +describe('MergedReportsComponent sorting', () => { + it.each([ + [ + 'Reported By', + ['Charlie', 'Alpha', 'Bravo'], + ['Bravo', 'Alpha', 'Charlie'], + ], + [ + 'Reported For', + ['Alpha', 'Charlie', 'Bravo'], + ['Bravo', 'Charlie', 'Alpha'], + ], + ['Reason', ['Alpha', 'Bravo', 'Charlie'], ['Charlie', 'Bravo', 'Alpha']], + [ + 'Report Time', + ['Charlie', 'Bravo', 'Alpha'], + ['Alpha', 'Bravo', 'Charlie'], + ], + ])( + 'sorts %s by visible semantics in ascending and descending order', + async (header, ascending, descending) => { + render( + + + + + , + ); + fireEvent.click(screen.getByRole('button', { name: /show/i })); + await screen.findByText('Zeta Policy'); + + const columnHeader = screen.getByRole('columnheader', { name: header }); + fireEvent.click(columnHeader); + expect(renderedReasons()).toEqual(ascending); + + fireEvent.click(columnHeader); + expect(renderedReasons()).toEqual(descending); + }, + ); +}); diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/MergedReportsComponent.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/MergedReportsComponent.tsx index c8d61e9d2..a2128935f 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/MergedReportsComponent.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/MergedReportsComponent.tsx @@ -10,6 +10,7 @@ import { ChevronDown, ChevronUp, ExternalLink } from 'lucide-react'; import { useCallback, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; +import { dateSort, stringSort } from '../../components/table/sort'; import Table from '../../components/table/Table'; import InvalidateReportsButton from './InvalidateReportsButton'; @@ -103,10 +104,30 @@ export default function MergedReportsComponent(props: { const columns = useMemo( () => [ - { header: 'Reported By', accessorKey: 'reportedBy' }, - { header: 'Reported For', accessorKey: 'reportedFor' }, - { header: 'Reason', accessorKey: 'reason' }, - { header: 'Report Time', accessorKey: 'reportTime' }, + { + header: 'Reported By', + accessorKey: 'reportedBy', + sortFn: stringSort, + sortDescFirst: false, + }, + { + header: 'Reported For', + accessorKey: 'reportedFor', + sortFn: stringSort, + sortDescFirst: false, + }, + { + header: 'Reason', + accessorKey: 'reason', + sortFn: stringSort, + sortDescFirst: false, + }, + { + header: 'Report Time', + accessorKey: 'reportTime', + sortFn: dateSort('reportTime'), + sortDescFirst: false, + }, ], [], ); @@ -141,6 +162,8 @@ export default function MergedReportsComponent(props: { hasReporter && report.displayInfo?.typeName ? `${report.displayInfo.typeName}: ` : ''; + const reportedFor = policy?.name ?? '—'; + const reason = report.reason?.trim() || '—'; return { reportedBy: (
@@ -204,6 +227,12 @@ export default function MergedReportsComponent(props: { reportTime: parseDatetimeToReadableStringInCurrentTimeZone( report.reportedAt, ), + values: { + reportedBy: `${reportedByPrefix}${reportedByLabel}`, + reportedFor, + reason, + reportTime: report.reportedAt, + }, }; }); }, [