From 704f78974f18610117bb19dcf54ef040b45625ca Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 21 Oct 2026 10:47:14 +1100 Subject: [PATCH] PM-6628: Fix remaining Marathon Match modal QA issues What was broken The earlier Community App and opportunities fixes updated the MM dialogs, permissions, and ZIP downloads. QA still found a missing example artifact for active matches, mismatched icons, uncentered modal rows, unbolded labels, sort arrows on inactive columns, left-aligned ranks, and table scrolling. Root cause The filter expected the literal example ID, but the dev API returns IDs such as YtoH-k9To2Ggxx-example. Global CSS resets inherited label weight and cell alignment. Generic icons and a forced 1200px table width retained visual gaps. What was changed Accept the exact submission-prefixed example ID while retaining original identifiers for downloads and excluding internal or unrelated artifacts. Reuse the folded-page icons from the merged Community App implementation. Explicitly center modal cells and bold metadata labels. Show sort arrows only on active challenge-detail columns, align desktop ranks right, reserve compact action space, and size columns to the card instead of forcing a minimum table width. Keep long handles on one line with a full-handle title. Preserve completed-match ZIP downloads, permissions, and modal navigation. Update the opportunities README. Any added/updated tests Add two regressions for prefixed example downloads and access changes. Use prefixed IDs in the ZIP-content test and verify active-only arrows in all six existing derived-column sorting cases. Update the icon assertion. All 65 opportunities suites and 814 tests pass. Lint, production build, and headless browser checks pass, including nine viewport widths, long handles, large history counts, modal styles, sorting, navigation, and real ZIP contents. The full suite has 398 passing suites and 2912 passing tests. Its 21 failed suites and 18 failed tests exactly match untouched dev at fc95c0f42; no new failures were introduced. --- src/apps/opportunities/README.md | 13 +++-- .../assets/submission-artifact-download.svg | 4 ++ .../src/assets/submission-artifact.svg | 4 ++ .../SubmissionArtifactsModal.module.scss | 2 + .../SubmissionArtifactsModal.spec.tsx | 50 ++++++++++++++++++ .../components/SubmissionArtifactsModal.tsx | 7 ++- .../SubmissionHistoryModal.module.scss | 6 +++ .../src/components/SubmissionHistoryModal.tsx | 3 +- .../pages/ChallengeDetailsPage.flows.spec.tsx | 12 ++++- .../pages/ChallengeDetailsPage.module.scss | 51 ++++++++++++++----- .../src/pages/ChallengeDetailsPage.tsx | 27 +++++++--- 11 files changed, 151 insertions(+), 28 deletions(-) create mode 100644 src/apps/opportunities/src/assets/submission-artifact-download.svg create mode 100644 src/apps/opportunities/src/assets/submission-artifact.svg diff --git a/src/apps/opportunities/README.md b/src/apps/opportunities/README.md index 2bcd85914..4d7f14740 100644 --- a/src/apps/opportunities/README.md +++ b/src/apps/opportunities/README.md @@ -814,7 +814,9 @@ Review App handoff. Artifact controls in All Submissions, My Submissions, and submission history use the challenge's exact `COMPLETED` status for Marathon Matches. Before completion (including cancelled challenges), contestants can list and download only their own -`example` artifact. After completion, registered contestants can download regular +`example` artifact, accepting both `example` and `-example` API IDs. +Downloads always use the original API ID; internal and unrelated IDs remain hidden. +After completion, registered contestants can download regular and internal provisional/system artifacts for their own and other members' attempts. Scores and phase end dates never unlock artifacts. Existing admin/copilot access is retained. The dialog filters cached entries when permission changes, and Review @@ -822,7 +824,8 @@ API independently authorizes both listing and every download. Completed MM dialo also offer Download all, packaging every returned artifact (including example-internal, example, provisional-internal, and system-internal when authorized) into one ZIP. If any download fails, no partial ZIP is saved. The artifact header shows the -submission ID and owner handle, including when opened from an older history entry. +bold Submission ID and Handle labels, including when opened from an older history entry. +Both modal tables vertically center their text and the shared folded-page artifact icons. The Marathon Match My Submissions table reserves enough width for the complete submission timestamp and keeps its date heading and sort icon on one line, aligned with the dates beneath it. Score columns remain right aligned. @@ -838,7 +841,11 @@ at that breakpoint. History requests include the selected member ID and submissi filter. Visible Marathon Matches expose full history to all viewers, including anonymous and unregistered visitors, through Review API. The History action shows the API's complete submission count in parentheses when available. In Marathon -Match All Submissions, the actions stay on one line: a file icon for artifacts +Match All Submissions, proportional columns avoid a forced desktop scrollbar, rank +values align right, and long handles stay on one line with ellipsis and a full-handle +title when space is limited. Rank headings wrap only when necessary. All challenge +detail tables show a sort arrow only on the active column; other headers remain +clickable. The actions stay on one line: a file icon for artifacts and a history icon followed by that count. Hovering shows `View Artifacts` or `History (8 submissions)` with the actual count (or `History` when unavailable); the buttons retain accessible names and open the existing dialogs. Artifact diff --git a/src/apps/opportunities/src/assets/submission-artifact-download.svg b/src/apps/opportunities/src/assets/submission-artifact-download.svg new file mode 100644 index 000000000..951c671da --- /dev/null +++ b/src/apps/opportunities/src/assets/submission-artifact-download.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/apps/opportunities/src/assets/submission-artifact.svg b/src/apps/opportunities/src/assets/submission-artifact.svg new file mode 100644 index 000000000..8b5253374 --- /dev/null +++ b/src/apps/opportunities/src/assets/submission-artifact.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/apps/opportunities/src/components/SubmissionArtifactsModal.module.scss b/src/apps/opportunities/src/components/SubmissionArtifactsModal.module.scss index fdf39b914..1771c38d2 100644 --- a/src/apps/opportunities/src/components/SubmissionArtifactsModal.module.scss +++ b/src/apps/opportunities/src/components/SubmissionArtifactsModal.module.scss @@ -51,6 +51,7 @@ line-height: 24px; padding: 8px 16px; text-align: left; + vertical-align: middle; } th { @@ -77,6 +78,7 @@ cursor: pointer; display: inline-flex; padding: 0; + vertical-align: middle; &:disabled { cursor: not-allowed; diff --git a/src/apps/opportunities/src/components/SubmissionArtifactsModal.spec.tsx b/src/apps/opportunities/src/components/SubmissionArtifactsModal.spec.tsx index 8ff269c23..9046c083d 100644 --- a/src/apps/opportunities/src/components/SubmissionArtifactsModal.spec.tsx +++ b/src/apps/opportunities/src/components/SubmissionArtifactsModal.spec.tsx @@ -102,6 +102,55 @@ describe('SubmissionArtifactsModal', () => { .not.toBeInTheDocument() }) + it('shows the active match example with its exact submission-prefixed API ID', async () => { + const submissionId = 'YtoH-k9To2Ggxx' + const exampleId = `${submissionId}-example` + mockedGetArtifacts.mockResolvedValue([ + exampleId, `${submissionId}-example-internal`, `${submissionId}-provisional-internal`, + `${submissionId}-system-internal`, 'other-submission-example', `${submissionId}-example-extra`, + ]) + render( + new Map() }}> + + , + ) + fireEvent.click(await screen.findByRole('button', { name: `Download artifact ${exampleId}` })) + expect(screen.getAllByRole('row')) + .toHaveLength(2) + expect(screen.queryByRole('button', { name: 'Download all' })).not.toBeInTheDocument() + await waitFor(() => expect(mockedDownloadArtifact) + .toHaveBeenCalledWith(submissionId, exampleId)) + await waitFor(() => expect(HTMLAnchorElement.prototype.click) + .toHaveBeenCalled()) + expect((HTMLAnchorElement.prototype.click as jest.Mock).mock.instances[0].download) + .toBe(`${exampleId}.zip`) + }) + + it('filters cached prefixed internal artifacts when management access is removed', async () => { + const artifactIds = ['submission-id-example', 'submission-id-example-internal'] + mockedGetArtifacts.mockResolvedValue(artifactIds) + const cache: Map = new Map() + const { rerender }: ReturnType = render( + cache }}> + + , + ) + await screen.findByRole('button', { name: `Download artifact ${artifactIds[1]}` }) + rerender( + cache }}> + + , + ) + await screen.findByRole('button', { name: `Download artifact ${artifactIds[0]}` }) + expect(screen.queryByRole('button', { name: `Download artifact ${artifactIds[1]}` })) + .not.toBeInTheDocument() + }) + it('loads and downloads the selected scorer artifact through Review API', async () => { render( new Map() }}> @@ -169,6 +218,7 @@ describe('SubmissionArtifactsModal', () => { it('shows submission metadata and saves all authorized artifacts in one complete ZIP', async () => { const artifactIds = ['example-internal', 'example', 'provisional-internal', 'system-internal'] + .map(id => `completed-submission-${id}`) const onClose = jest.fn() mockedGetArtifacts.mockResolvedValue(artifactIds) mockedDownloadArtifact.mockImplementation((_submissionId, id) => ( diff --git a/src/apps/opportunities/src/components/SubmissionArtifactsModal.tsx b/src/apps/opportunities/src/components/SubmissionArtifactsModal.tsx index a2a536f60..1e9fb2851 100644 --- a/src/apps/opportunities/src/components/SubmissionArtifactsModal.tsx +++ b/src/apps/opportunities/src/components/SubmissionArtifactsModal.tsx @@ -11,6 +11,7 @@ import { getChallengeSubmissionArtifacts, } from '../services' import { marathonRatingColor } from '../utils/marathon-match.utils' +import { ReactComponent as DownloadArtifactIcon } from '../assets/submission-artifact-download.svg' import modalStyles from './SubmissionHistoryModal.module.scss' import styles from './SubmissionArtifactsModal.module.scss' @@ -77,7 +78,8 @@ function saveArtifact(blob: Blob, filename: string): void { /** * Lists and downloads scorer-generated files for one authored submission. * - * Contestants see only example before completion; staff retain management access. + * Contestants see only example (bare or submission-prefixed ID) before completion; + * downloads retain the exact API identifier. Staff retain management access. * Completed matches offer one ZIP of all API-authorized artifacts. * @param props selected submission, visibility, and close callback. * @returns artifact dialog with loading, error, empty, and download states. @@ -118,6 +120,7 @@ export const SubmissionArtifactsModal: FC = props // Also filter cached responses when completion or viewer permissions change. const artifacts = (response.data ?? []).filter(artifactId => ( props.allowInternalArtifacts || artifactId.toLowerCase() === 'example' + || artifactId === `${props.submissionId}-example` )) /** @@ -181,7 +184,7 @@ export const SubmissionArtifactsModal: FC = props onClick={() => downloadArtifact(artifactId)} type='button' > -