From d3ca44e35e0381d558cbcf43287f8031705c15a5 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 2 Jul 2026 09:46:51 +1000 Subject: [PATCH] PM-5511: Add submission ID filter What was broken Challenge submission lists could only be filtered by handle, date range, and minimum score, so users with a submission ID from logs had no direct way to find the matching row and submitter. Root cause The submissions section filter state and filter predicate did not include submission identifiers, even though the table displays each submission ID. What was changed Added a Submission ID text filter next to the Handle filter. Matched the filter against current and legacy submission IDs with case-insensitive partial matching before pagination. Any added/updated tests Added a SubmissionsSection test covering filtering by current and legacy submission IDs. --- .../SubmissionsSection.spec.tsx | 200 ++++++++++++++++++ .../SubmissionsSection/SubmissionsSection.tsx | 46 ++++ 2 files changed, 246 insertions(+) create mode 100644 src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.spec.tsx diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.spec.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.spec.tsx new file mode 100644 index 000000000..8f2e1f372 --- /dev/null +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.spec.tsx @@ -0,0 +1,200 @@ +/* eslint-disable global-require, import/no-extraneous-dependencies */ +/* eslint-disable @typescript-eslint/no-var-requires, ordered-imports/ordered-imports */ +import { + fireEvent, + render, + screen, +} from '@testing-library/react' + +import type { + Challenge, + Submission, +} from '../../../../../lib/models' + +import { SubmissionsSection } from './SubmissionsSection' + +const mockUseDownloadAllSubmissions = jest.fn() +const mockUseDownloadSubmission = jest.fn() +const mockUseFetchSubmissions = jest.fn() +const mockFetchMembersByUserIds = jest.fn() + +jest.mock('~/libs/ui', () => ({ + Button: (props: { + disabled?: boolean + label: string + onClick?: () => void + }): JSX.Element => ( + + ), +}), { + virtual: true, +}) + +jest.mock('../../../../../lib/assets/icons/lock.svg', () => ({ + ReactComponent: (): JSX.Element => , +})) + +jest.mock('../../../../../lib/components', () => ({ + ArtifactsModal: (): JSX.Element =>
Artifacts modal
, + Pagination: (props: { total: number }): JSX.Element => ( +
{props.total}
+ ), + SubmissionRunnerLogsModal: (): JSX.Element =>
Runner logs modal
, + SubmissionsTable: (props: { submissions: Submission[] }): JSX.Element => ( +
+ {props.submissions.map(submission => ( +
+ {submission.id} + {submission.memberHandle} +
+ ))} +
+ ), +})) + +jest.mock('../../../../../lib/constants', () => ({ + PAGE_SIZE: 10, +})) + +jest.mock('../../../../../lib/contexts', () => { + const React = require('react') as typeof import('react') + + return { + WorkAppContext: React.createContext({ + isAdmin: false, + isAnonymous: false, + isCopilot: false, + isManager: false, + isReadOnly: false, + loginUserInfo: undefined, + userRoles: [], + }), + } +}) + +jest.mock('../../../../../lib/hooks', () => ({ + useDownloadAllSubmissions: (): unknown => mockUseDownloadAllSubmissions(), + useDownloadSubmission: (): unknown => mockUseDownloadSubmission(), + useFetchSubmissions: (...args: unknown[]): unknown => mockUseFetchSubmissions(...args), +})) + +jest.mock('../../../../../lib/services', () => ({ + fetchMembersByUserIds: (...args: unknown[]): unknown => mockFetchMembersByUserIds(...args), +})) + +jest.mock('../../../../../lib/utils', () => ({ + canDownloadSubmissions: (): boolean => false, + canViewMarathonMatchRunnerLogs: (): boolean => false, + getSubmissionFinalScore: (): number => 0, + getSubmissionInitialScore: (): number => 0, + getSubmissionProvisionalScore: (): number => 0, + getSubmissionSystemScore: (): number => 0, + isMarathonMatchChallenge: (): boolean => false, + showErrorToast: jest.fn(), +})) + +jest.mock('./SubmissionsSection.module.scss', () => new Proxy({}, { + get: (_target, property) => String(property), +})) + +const baseChallenge: Challenge = { + id: 'challenge-1', + name: 'Challenge', + status: 'ACTIVE', +} + +const submissions: Submission[] = [ + { + challengeId: 'challenge-1', + createdAt: '2026-07-01T10:00:00.000Z', + createdBy: 'member-1', + id: 'alpha-submission', + memberHandle: 'alpha', + type: 'SUBMISSION', + }, + { + challengeId: 'challenge-1', + createdAt: '2026-07-01T11:00:00.000Z', + createdBy: 'member-2', + id: 'bravo-submission', + memberHandle: 'bravo', + type: 'SUBMISSION', + }, + { + challengeId: 'challenge-1', + createdAt: '2026-07-01T12:00:00.000Z', + createdBy: 'member-3', + id: 'charlie-submission', + legacySubmissionId: 'legacy-charlie-id', + memberHandle: 'charlie', + type: 'SUBMISSION', + }, +] + +describe('SubmissionsSection', () => { + beforeEach(() => { + jest.clearAllMocks() + + mockUseDownloadAllSubmissions.mockReturnValue({ + downloadAll: jest.fn(), + isDownloading: false, + progress: 0, + }) + mockUseDownloadSubmission.mockReturnValue({ + downloadSubmission: jest.fn(), + isLoading: {}, + }) + mockUseFetchSubmissions.mockReturnValue({ + error: undefined, + isError: false, + isLoading: false, + mutate: jest.fn(), + submissions, + total: submissions.length, + }) + mockFetchMembersByUserIds.mockResolvedValue([]) + }) + + it('filters submissions by current or legacy submission ID', () => { + render( + , + ) + + fireEvent.change(screen.getByLabelText('Submission ID'), { + target: { + value: 'BRAVO', + }, + }) + + expect(screen.getByText('bravo-submission')) + .toBeTruthy() + expect(screen.queryByText('alpha-submission')) + .toBeNull() + expect(screen.queryByText('charlie-submission')) + .toBeNull() + expect(screen.getByTestId('pagination-total').textContent) + .toBe('1') + + fireEvent.change(screen.getByLabelText('Submission ID'), { + target: { + value: 'legacy-charlie', + }, + }) + + expect(screen.getByText('charlie-submission')) + .toBeTruthy() + expect(screen.queryByText('alpha-submission')) + .toBeNull() + expect(screen.queryByText('bravo-submission')) + .toBeNull() + }) +}) diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.tsx index 70662c63d..5d33d59e5 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.tsx @@ -57,6 +57,7 @@ interface FilterState { handle: string minScore: string startDate: string + submissionId: string } interface SubmissionsSectionProps { @@ -330,6 +331,33 @@ function matchesFilterHandle(submission: Submission, handleFilter: string): bool .includes(normalizedHandleFilter) } +/** + * Checks whether a submission matches the submission ID filter. + * @param submission Submission row being evaluated. + * @param submissionIdFilter Case-insensitive partial submission ID entered by the user. + * @returns True when the filter is empty or matches the submission's current or legacy ID. + * Used by `matchesFilters` before the submissions table is paginated. + */ +function matchesFilterSubmissionId( + submission: Submission, + submissionIdFilter: string, +): boolean { + if (!submissionIdFilter) { + return true + } + + const normalizedSubmissionIdFilter = normalizeValue(submissionIdFilter) + .toLowerCase() + + return [ + submission.id, + submission.legacySubmissionId, + ] + .some(submissionId => normalizeValue(submissionId) + .toLowerCase() + .includes(normalizedSubmissionIdFilter)) +} + function matchesFilters( submission: Submission, filters: FilterState, @@ -339,6 +367,10 @@ function matchesFilters( return false } + if (!matchesFilterSubmissionId(submission, filters.submissionId)) { + return false + } + if (!matchesFilterDateRange(submission, filters.startDate, filters.endDate)) { return false } @@ -361,6 +393,7 @@ export const SubmissionsSection: FC = ( handle: '', minScore: '', startDate: '', + submissionId: '', }) const [memberCache, setMemberCache] = useState({}) const [page, setPage] = useState(1) @@ -642,6 +675,19 @@ export const SubmissionsSection: FC = ( /> + +