Add Evidence Management Dashboard#5057
Conversation
…aim-dash-cherry-claim
Summary by CodeRabbit
WalkthroughAdds backend claim/evidence GraphQL response changes, frontend query and mutation documents, reusable claim/evidence forms and actions, candidate dashboard and claim/evidence pages, and expanded unit and e2e coverage across those routes. ChangesCandidate Claim Management Dashboard
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## feature/bod-candidate-transparency #5057 +/- ##
======================================================================
- Coverage 98.47% 98.46% -0.01%
======================================================================
Files 550 556 +6
Lines 17675 17995 +320
Branches 2479 2539 +60
======================================================================
+ Hits 17405 17719 +314
- Misses 166 168 +2
- Partials 104 108 +4
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 15
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@backend/tests/unit/apps/owasp/api/internal/queries/board_candidate_claim_test.py`:
- Around line 29-30: The board candidate claim tests currently only stub the
queryset chain and do not verify the optimization contract. In the
`board_candidate_claims` test cases, assert that `annotate()` is called with an
`evidence_exists` keyword argument (along with the existing queryset chaining)
so the tests fail if the annotation is removed and the code falls back to
per-claim `.exists()` queries. Use the existing `mock_qs` setup in
`test_board_candidate_claims` to check the `annotate` call directly.
In `@e2e/helpers/mockClaimAuth.ts`:
- Around line 17-18: `mockClaimAuth` is unconditionally parsing request bodies
as JSON, which crashes on multipart `UploadHttpLink` uploads before the
`operationName` check. Update the `page.route('**/graphql/', ...)` handler to
inspect `request.headers()` first, only call `request.postDataJSON()` when the
content type is JSON, and safely skip or specially handle multipart requests so
evidence upload tests do not fail.
In `@e2e/pages/BoardCandidateClaimCreate.spec.ts`:
- Around line 8-22: The mocked createBoardCandidateClaim payload in
BoardCandidateClaimCreate.spec should match the GraphQL result shape used by the
cache update path. Update the CreateBoardCandidateClaimResult mock so it uses
the same claim object contract as production and includes the list fields
expected by GetBoardCandidateClaimsDocument, such as order and hasEvidence,
instead of returning a simplified BoardCandidateClaimNode shape. Keep the
response aligned with the createBoardCandidateClaim and claim symbols so the E2E
test exercises the real cache-append behavior.
In `@e2e/pages/BoardCandidateClaimEdit.spec.ts`:
- Around line 13-24: The edit E2E mock is using the wrong GraphQL typename for
updateBoardCandidateClaim. Update the mock in BoardCandidateClaimEdit.spec.ts to
match the shared ClaimResult contract instead of UpdateBoardCandidateClaimResult
so it stays aligned with the generated mutation and Apollo cache behavior. Keep
the existing update payload shape, but change the __typename on the
updateBoardCandidateClaim result to the real mutation result type used by the
schema.
In `@frontend/__tests__/mockData/mockClaimData.ts`:
- Around line 72-79: Keep mockSingleClaim aligned with the updated claim fixture
shape by adding the missing hasEvidence field so it matches mockClaims and the
current claim model. Update the mock in mockClaimData to include the same fields
used by the claim detail flow, and keep the object structure consistent with the
claim-related test data to avoid fixture drift.
In `@frontend/__tests__/mockData/mockEvidenceData.ts`:
- Around line 1-57: The claim/evidence test fixtures are duplicated in
mockEvidenceData and mockClaimData with slightly different shapes, which will
drift over time. Refactor the shared fixtures by introducing a single base
claim/evidence object or small builder used by both mockEvidence, mockEvidences,
mockClaimForEvidence, and the related mockGet*Data exports, and only override
fields like id and __typename where needed so the two test data modules stay
consistent.
In `@frontend/package.json`:
- Line 22: The new apollo-upload-client dependency in package.json is compatible
with ESM, but the repo’s Node requirement is pinned to ^24.0.0, so make sure the
CI/CD workflow and local dev setup are explicitly using Node 24. Update the
relevant environment/version config used by the frontend build and test jobs,
and verify any Node version manager or setup scripts align with the engines.node
constraint to prevent build or runtime mismatches.
In `@frontend/src/app/board/`[year]/candidates/[login]/claims/create/page.tsx:
- Around line 48-56: Handle candidateQueryError before the access-denied check
in the create claim page so transient fetch failures do not look like
authorization failures. Update the create page component to mirror the edit
page’s error handling by adding an explicit render branch for
candidateQueryError before computing or using isCandidate, and only fall through
to AccessDeniedDisplay when the query succeeded but the user is not allowed.
In `@frontend/src/app/board/`[year]/candidates/[login]/claims/page.tsx:
- Around line 137-157: The Apollo cache update in the reorder mutation is
targeting the wrong query document, so the UI stays stale after a successful
reorder. In the update callback inside the claims page, change both
cache.readQuery and cache.writeQuery to use GetBoardCandidateAndClaimsDocument
instead of GetBoardCandidateClaimsDocument, keeping the same login/year
variables, so the cached result matches the query actually used by the page and
derived state refreshes correctly.
In `@frontend/src/components/ClaimForm.tsx`:
- Around line 87-92: The catch block in ClaimForm’s submit flow is swallowing
mixed GraphQL failures by only checking hasValidationErrors. Update the error
handling around extractGraphQLErrors() to also inspect unmappedErrors, and only
suppress the exception when every GraphQL error was mapped to a field. If any
unmappedErrors remain, still call setBackendErrors(validationErrors) for the
mapped fields but rethrow the original error so the page-level toast path can
handle the unmapped/server error.
In `@frontend/src/components/EvidenceActions.tsx`:
- Around line 66-82: The cache update in EvidenceActions is targeting the wrong
GraphQL document, so the destination claim page stays stale and refetches
unnecessarily. Update the Apollo update handler in EvidenceActions to write the
removed evidence into the cache entry used by the claim details page, namely
GetClaimAndEvidencesDocument, not GetBoardCandidateClaimEvidencesDocument. Make
sure the variables and response shape match the page query in page.tsx
(including the nested boardCandidateClaim structure) so the evidence list
updates correctly after the redirect.
In `@frontend/src/components/EvidenceForm.tsx`:
- Line 66: Rename the typoed handler in EvidenceForm from handleFilechange to
handleFileChange to match standard camelCase and keep naming consistent; update
the function declaration and any references in the component so the event
handler symbol remains aligned everywhere.
- Around line 179-187: The file picker filtering in the EvidenceForm component
is using extension values without the required leading dots, so the native
accept filter is ineffective. Update the accept value passed to FormFileInput in
EvidenceForm to transform EVIDENCE_ALLOWED_EXTENSIONS into dotted extensions
before joining them, while keeping the existing runtime validation and
handleFilechange behavior unchanged.
In `@frontend/src/components/forms/shared/formValidationUtils.ts`:
- Around line 39-44: The file extension check in formValidationUtils is treating
extension-less filenames as if the whole filename were the extension, which
produces a misleading message. Update the validation logic around the file.name
split/pop handling to explicitly detect when there is no dot in the name and
return a clearer no-extension error before comparing against allowedExtensions.
While touching this block, use a separate allowed string in the same validation
helper to avoid the nested template literal warning in the return message.
In `@frontend/src/types/claim.ts`:
- Around line 3-14: The Claim type is weakening the GraphQL contract by making
backend-provided fields optional; update the frontend type to stay aligned with
the generated operation type instead of hand-typing these fields. In Claim, make
createdAt, hasEvidence, order, and updatedAt required (or replace this alias
with the generated GraphQL type used by the claims dashboard and cache updates)
so Claim, ClaimStatusEnum, and related consumers remain in lockstep.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: bce348ca-a335-464e-be8e-96702ea37bc7
⛔ Files ignored due to path filters (7)
frontend/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlfrontend/src/types/__generated__/boardQueries.generated.tsis excluded by!**/__generated__/**frontend/src/types/__generated__/claimMutations.generated.tsis excluded by!**/__generated__/**frontend/src/types/__generated__/claimQueries.generated.tsis excluded by!**/__generated__/**frontend/src/types/__generated__/evidenceMutations.generated.tsis excluded by!**/__generated__/**frontend/src/types/__generated__/evidenceQueries.generated.tsis excluded by!**/__generated__/**frontend/src/types/__generated__/graphql.tsis excluded by!**/__generated__/**
📒 Files selected for processing (54)
backend/apps/owasp/api/internal/mutations/board_candidate_claim.pybackend/apps/owasp/api/internal/nodes/board_candidate_claim.pybackend/apps/owasp/api/internal/queries/board_candidate_claim.pybackend/tests/unit/apps/owasp/api/internal/mutations/board_candidate_claim_test.pybackend/tests/unit/apps/owasp/api/internal/nodes/board_candidate_claim_test.pybackend/tests/unit/apps/owasp/api/internal/queries/board_candidate_claim_test.pye2e/helpers/mockAuthCookies.tse2e/helpers/mockClaimAuth.tse2e/helpers/mockDashboardCookies.tse2e/pages/BoardCandidateClaimCreate.spec.tse2e/pages/BoardCandidateClaimDetails.spec.tse2e/pages/BoardCandidateClaimEdit.spec.tse2e/pages/BoardCandidateClaimEvidenceCreate.spec.tse2e/pages/BoardCandidateClaimEvidenceDetails.spec.tse2e/pages/BoardCandidateClaimEvidenceEdit.spec.tse2e/pages/BoardCandidateClaims.spec.tsfrontend/__tests__/mockData/mockClaimData.tsfrontend/__tests__/mockData/mockEvidenceData.tsfrontend/__tests__/unit/components/ActionButton.test.tsxfrontend/__tests__/unit/components/ClaimActions.test.tsxfrontend/__tests__/unit/components/DropdownActions.test.tsxfrontend/__tests__/unit/components/EvidenceActions.test.tsxfrontend/__tests__/unit/components/EvidenceForm.test.tsxfrontend/__tests__/unit/pages/BoardCandidatesPage.test.tsxfrontend/__tests__/unit/pages/CandidateClaimsPage.test.tsxfrontend/__tests__/unit/pages/ClaimDetailsPage.test.tsxfrontend/__tests__/unit/pages/CreateClaimPage.test.tsxfrontend/__tests__/unit/pages/CreateEvidencePage.test.tsxfrontend/__tests__/unit/pages/EditClaimPage.test.tsxfrontend/__tests__/unit/pages/EditEvidencePage.test.tsxfrontend/__tests__/unit/pages/EvidenceDetailsPage.test.tsxfrontend/package.jsonfrontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/edit/page.tsxfrontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/evidences/[evidenceKey]/edit/page.tsxfrontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/evidences/[evidenceKey]/page.tsxfrontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/evidences/create/page.tsxfrontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/page.tsxfrontend/src/app/board/[year]/candidates/[login]/claims/create/page.tsxfrontend/src/app/board/[year]/candidates/[login]/claims/page.tsxfrontend/src/app/board/[year]/candidates/page.tsxfrontend/src/components/ClaimActions.tsxfrontend/src/components/ClaimForm.tsxfrontend/src/components/DropdownActions.tsxfrontend/src/components/EvidenceActions.tsxfrontend/src/components/EvidenceForm.tsxfrontend/src/components/forms/shared/FormFileInput.tsxfrontend/src/components/forms/shared/formValidationUtils.tsfrontend/src/server/mutations/claimMutations.tsfrontend/src/server/mutations/evidenceMutations.tsfrontend/src/server/queries/boardQueries.tsfrontend/src/server/queries/claimQueries.tsfrontend/src/server/queries/evidenceQueries.tsfrontend/src/types/claim.tsfrontend/src/utils/helpers/apolloClient.ts
| mock_qs.annotate.return_value = mock_qs | ||
| mock_qs.order_by.return_value = claims |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
Assert the evidence_exists annotation, not just the chain.
These tests now only stub annotate(), so they will still pass if board_candidate_claims stops annotating and the dashboard falls back to one .exists() query per claim. Please assert that annotate is called with an evidence_exists kwarg to lock in the optimization contract.
Also applies to: 52-53, 73-74
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@backend/tests/unit/apps/owasp/api/internal/queries/board_candidate_claim_test.py`
around lines 29 - 30, The board candidate claim tests currently only stub the
queryset chain and do not verify the optimization contract. In the
`board_candidate_claims` test cases, assert that `annotate()` is called with an
`evidence_exists` keyword argument (along with the existing queryset chaining)
so the tests fail if the annotation is removed and the code falls back to
per-claim `.exists()` queries. Use the existing `mock_qs` setup in
`test_board_candidate_claims` to check the `annotate` call directly.
| await page.route('**/graphql/', async (route, request) => { | ||
| const postData = request.postDataJSON() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\nUsages of mockClaimAuth:\n'
rg -n --type=ts --type=tsx '\bmockClaimAuth\s*\(' e2e frontend
printf '\nPotential multipart/upload GraphQL paths:\n'
rg -n --type=ts --type=tsx 'createUploadLink|multipart/form-data|FormData|Upload|postDataJSON\(' e2e frontendRepository: OWASP/Nest
Length of output: 202
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Usages of mockClaimAuth ==="
rg -n '\bmockClaimAuth\s*\(' --type ts --type ts --glob "*test*" --glob "*.spec.ts" --glob "*e2e*" e2e frontend || true
echo "=== Potential multipart/upload GraphQL patterns ==="
rg -n 'createUploadLink|multipart/form-data|new FormData|Upload' --type ts e2e frontend || true
echo "=== Check mockClaimAuth definition ==="
cat -n e2e/helpers/mockClaimAuth.ts | head -40Repository: OWASP/Nest
Length of output: 4932
Fix postDataJSON() crash on multipart file uploads in mockClaimAuth.
The mockClaimAuth helper unconditionally calls request.postDataJSON() on line 18. Since the frontend uses UploadHttpLink for evidence flows, requests involving files are sent as multipart/form-data. Playwright throws an error when calling postDataJSON() on non-JSON bodies, causing tests like BoardCandidateClaimEvidenceCreate.spec.ts to fail before the operationName filter runs.
Gate the JSON parsing behind a content-type check:
const headers = request.headers()
const contentType = headers['content-type'] || ''
let postData
if (contentType.includes('application/json')) {
postData = request.postDataJSON()
} else {
// For multipart uploads, parse body as text and extract operationName manually
postData = { operationName: null }
// Alternative: skip operationName check for non-JSON or parse text body if needed
}Update the route handler to conditionally access postData only when safe.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@e2e/helpers/mockClaimAuth.ts` around lines 17 - 18, `mockClaimAuth` is
unconditionally parsing request bodies as JSON, which crashes on multipart
`UploadHttpLink` uploads before the `operationName` check. Update the
`page.route('**/graphql/', ...)` handler to inspect `request.headers()` first,
only call `request.postDataJSON()` when the content type is JSON, and safely
skip or specially handle multipart requests so evidence upload tests do not
fail.
| createBoardCandidateClaim: { | ||
| __typename: 'CreateBoardCandidateClaimResult', | ||
| ok: true, | ||
| message: 'Claim created successfully!', | ||
| claim: { | ||
| __typename: 'BoardCandidateClaimNode', | ||
| id: 'new-claim', | ||
| key: 'new-claim', | ||
| name: 'Test Claim', | ||
| description: 'Test description', | ||
| status: 'DRAFT', | ||
| createdAt: '2025-01-15T10:00:00Z', | ||
| updatedAt: '2025-01-15T10:00:00Z', | ||
| }, | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make the mocked create payload match the GraphQL contract.
The page appends createBoardCandidateClaim.claim straight into GetBoardCandidateClaimsDocument, but this mock returns a different result typename and omits list fields like order/hasEvidence. That weakens the only E2E path covering the cache update because the mocked response shape no longer matches what production returns.
Suggested fix
createBoardCandidateClaim: {
- __typename: 'CreateBoardCandidateClaimResult',
+ __typename: 'ClaimResult',
ok: true,
+ code: null,
message: 'Claim created successfully!',
claim: {
__typename: 'BoardCandidateClaimNode',
id: 'new-claim',
key: 'new-claim',
name: 'Test Claim',
description: 'Test description',
+ hasEvidence: false,
+ order: 1,
status: 'DRAFT',
createdAt: '2025-01-15T10:00:00Z',
updatedAt: '2025-01-15T10:00:00Z',
},
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| createBoardCandidateClaim: { | |
| __typename: 'CreateBoardCandidateClaimResult', | |
| ok: true, | |
| message: 'Claim created successfully!', | |
| claim: { | |
| __typename: 'BoardCandidateClaimNode', | |
| id: 'new-claim', | |
| key: 'new-claim', | |
| name: 'Test Claim', | |
| description: 'Test description', | |
| status: 'DRAFT', | |
| createdAt: '2025-01-15T10:00:00Z', | |
| updatedAt: '2025-01-15T10:00:00Z', | |
| }, | |
| }, | |
| createBoardCandidateClaim: { | |
| __typename: 'ClaimResult', | |
| ok: true, | |
| code: null, | |
| message: 'Claim created successfully!', | |
| claim: { | |
| __typename: 'BoardCandidateClaimNode', | |
| id: 'new-claim', | |
| key: 'new-claim', | |
| name: 'Test Claim', | |
| description: 'Test description', | |
| hasEvidence: false, | |
| order: 1, | |
| status: 'DRAFT', | |
| createdAt: '2025-01-15T10:00:00Z', | |
| updatedAt: '2025-01-15T10:00:00Z', | |
| }, | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@e2e/pages/BoardCandidateClaimCreate.spec.ts` around lines 8 - 22, The mocked
createBoardCandidateClaim payload in BoardCandidateClaimCreate.spec should match
the GraphQL result shape used by the cache update path. Update the
CreateBoardCandidateClaimResult mock so it uses the same claim object contract
as production and includes the list fields expected by
GetBoardCandidateClaimsDocument, such as order and hasEvidence, instead of
returning a simplified BoardCandidateClaimNode shape. Keep the response aligned
with the createBoardCandidateClaim and claim symbols so the E2E test exercises
the real cache-append behavior.
| updateBoardCandidateClaim: { | ||
| __typename: 'UpdateBoardCandidateClaimResult', | ||
| ok: true, | ||
| message: 'Claim updated successfully!', | ||
| claim: { | ||
| __typename: 'BoardCandidateClaimNode', | ||
| ...mockSingleClaim, | ||
| name: 'Updated Leadership', | ||
| description: 'Updated description', | ||
| updatedAt: '2025-01-20T10:00:00Z', | ||
| }, | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use the real mutation result typename in the edit mock.
updateBoardCandidateClaim now returns the shared ClaimResult contract, not UpdateBoardCandidateClaimResult. Keeping the E2E mock aligned with the schema matters here because these pages rely on generated GraphQL documents and Apollo cache behavior.
Suggested fix
updateBoardCandidateClaim: {
- __typename: 'UpdateBoardCandidateClaimResult',
+ __typename: 'ClaimResult',
ok: true,
+ code: null,
message: 'Claim updated successfully!',
claim: {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| updateBoardCandidateClaim: { | |
| __typename: 'UpdateBoardCandidateClaimResult', | |
| ok: true, | |
| message: 'Claim updated successfully!', | |
| claim: { | |
| __typename: 'BoardCandidateClaimNode', | |
| ...mockSingleClaim, | |
| name: 'Updated Leadership', | |
| description: 'Updated description', | |
| updatedAt: '2025-01-20T10:00:00Z', | |
| }, | |
| }, | |
| updateBoardCandidateClaim: { | |
| __typename: 'ClaimResult', | |
| ok: true, | |
| code: null, | |
| message: 'Claim updated successfully!', | |
| claim: { | |
| __typename: 'BoardCandidateClaimNode', | |
| ...mockSingleClaim, | |
| name: 'Updated Leadership', | |
| description: 'Updated description', | |
| updatedAt: '2025-01-20T10:00:00Z', | |
| }, | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@e2e/pages/BoardCandidateClaimEdit.spec.ts` around lines 13 - 24, The edit E2E
mock is using the wrong GraphQL typename for updateBoardCandidateClaim. Update
the mock in BoardCandidateClaimEdit.spec.ts to match the shared ClaimResult
contract instead of UpdateBoardCandidateClaimResult so it stays aligned with the
generated mutation and Apollo cache behavior. Keep the existing update payload
shape, but change the __typename on the updateBoardCandidateClaim result to the
real mutation result type used by the schema.
| export const mockSingleClaim = { | ||
| key: 'experience-leadership', | ||
| name: 'Leadership Experience', | ||
| description: 'Experience in leadership.', | ||
| status: 'DRAFT', | ||
| createdAt: '2025-01-15T10:00:00Z', | ||
| updatedAt: '2025-01-15T10:00:00Z', | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Keep mockSingleClaim aligned with the new claim shape.
mockClaims includes hasEvidence, but mockSingleClaim drops it. That leaves the detail-page tests blind to the new field added in this PR and makes fixture drift easy to miss.
Suggested change
export const mockSingleClaim = {
key: 'experience-leadership',
name: 'Leadership Experience',
description: 'Experience in leadership.',
status: 'DRAFT',
+ hasEvidence: true,
createdAt: '2025-01-15T10:00:00Z',
updatedAt: '2025-01-15T10:00:00Z',
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const mockSingleClaim = { | |
| key: 'experience-leadership', | |
| name: 'Leadership Experience', | |
| description: 'Experience in leadership.', | |
| status: 'DRAFT', | |
| createdAt: '2025-01-15T10:00:00Z', | |
| updatedAt: '2025-01-15T10:00:00Z', | |
| } | |
| export const mockSingleClaim = { | |
| key: 'experience-leadership', | |
| name: 'Leadership Experience', | |
| description: 'Experience in leadership.', | |
| status: 'DRAFT', | |
| hasEvidence: true, | |
| createdAt: '2025-01-15T10:00:00Z', | |
| updatedAt: '2025-01-15T10:00:00Z', | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/__tests__/mockData/mockClaimData.ts` around lines 72 - 79, Keep
mockSingleClaim aligned with the updated claim fixture shape by adding the
missing hasEvidence field so it matches mockClaims and the current claim model.
Update the mock in mockClaimData to include the same fields used by the claim
detail flow, and keep the object structure consistent with the claim-related
test data to avoid fixture drift.
| update(cache) { | ||
| const existing = cache.readQuery<GetBoardCandidateClaimEvidencesQuery>({ | ||
| query: GetBoardCandidateClaimEvidencesDocument, | ||
| variables: { claimKey: claim.key, login, year: Number.parseInt(year) }, | ||
| }) | ||
| if (existing) { | ||
| cache.writeQuery({ | ||
| query: GetBoardCandidateClaimEvidencesDocument, | ||
| variables: { claimKey: claim.key, login, year: Number.parseInt(year) }, | ||
| data: { | ||
| boardCandidateClaimEvidences: existing.boardCandidateClaimEvidences.filter( | ||
| (e) => e.key !== evidence.key | ||
| ), | ||
| }, | ||
| }) | ||
| } | ||
| }, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find which query document the claim details page (redirect destination) uses to render evidences.
fd -t f 'page.tsx' frontend/src/app/board --exec rg -n -C2 'GetClaimAndEvidencesDocument|GetBoardCandidateClaimEvidencesDocument' {}
# Locate the claim details route specifically
fd -t f 'page.tsx' "frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]" 2>/dev/nullRepository: OWASP/Nest
Length of output: 2607
🏁 Script executed:
cat -n frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/page.tsx | head -50Repository: OWASP/Nest
Length of output: 2468
Incorrect cache update causes stale UI and unnecessary network requests on the destination page
The update function writes to the cache using GetBoardCandidateClaimEvidencesDocument, but the redirect destination (/board/[year]/candidates/[login]/claims/[claimKey]) reads data using GetClaimAndEvidencesDocument.
Because these are distinct GraphQL documents, the cache.writeQuery call fails to update the evidence list for the destination page. frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/page.tsx will continue to display the removed evidence and trigger an unnecessary network refetch due to its cache-and-network policy.
Update the update function to write to the correct document used by the details page:
Suggested change
// Change the query in cache.writeQuery to match the destination page
cache.writeQuery({
query: GetClaimAndEvidencesDocument, // <- Use this instead of GetBoardCandidateClaimEvidencesDocument
variables: { key: claim.key, login, year: Number.parseInt(year) },
data: {
boardCandidateClaim: {
...existing.boardCandidateClaim,
boardCandidateClaimEvidences: existing.boardCandidateClaimEvidences.filter(
(e) => e.key !== evidence.key
),
},
},
})Note: Adjust the data shape to match the GetClaimAndEvidencesDocument response structure (likely nested under boardCandidateClaim).
Alternatively, consider invalidating the cache for both documents or using cache.modify with a typed field policy if the data shapes allow a unified update.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/EvidenceActions.tsx` around lines 66 - 82, The cache
update in EvidenceActions is targeting the wrong GraphQL document, so the
destination claim page stays stale and refetches unnecessarily. Update the
Apollo update handler in EvidenceActions to write the removed evidence into the
cache entry used by the claim details page, namely GetClaimAndEvidencesDocument,
not GetBoardCandidateClaimEvidencesDocument. Make sure the variables and
response shape match the page query in page.tsx (including the nested
boardCandidateClaim structure) so the evidence list updates correctly after the
redirect.
| } | ||
| } | ||
|
|
||
| const handleFilechange = (e: React.ChangeEvent<HTMLInputElement>) => { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Typo in handler name: handleFilechange → handleFileChange.
Minor naming consistency for camelCase.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/EvidenceForm.tsx` at line 66, Rename the typoed
handler in EvidenceForm from handleFilechange to handleFileChange to match
standard camelCase and keep naming consistent; update the function declaration
and any references in the component so the event handler symbol remains aligned
everywhere.
| <FormFileInput | ||
| id="evidence-file" | ||
| label="File (optional)" | ||
| onChange={handleFilechange} | ||
| accept={EVIDENCE_ALLOWED_EXTENSIONS.join(',')} | ||
| selectedFile={formData.file} | ||
| error={fileError} | ||
| touched={true} | ||
| /> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor
🧩 Analysis chain
🌐 Web query:
HTML input accept attribute valid values file extension with or without leading dot
💡 Result:
When using the accept attribute for an HTML file input, valid file extension specifiers must begin with a period (.) [1][2]. According to web standards and documentation, a valid filename extension specifier is defined as a case-insensitive extension starting with a period character, such as.jpg,.pdf, or.doc [1][3]. Using an extension without the leading dot (e.g., jpg instead of.jpg) is not considered a valid extension specifier for the accept attribute [1][2]. If you provide an invalid format, the browser may ignore that specific part of the attribute value or fail to correctly trigger the file type filter in the system file picker. To ensure cross-browser compatibility and correct behavior, always include the leading period for file extensions [1][2]. In addition to file extensions, the accept attribute also accepts valid MIME type strings (e.g., image/png) and shorthand wildcards (e.g., image/, audio/, or video/*) [1][3]. These can be mixed with file extensions in a comma-separated list [1][4].
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept
- 2: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file
- 3: https://developer.mozilla.org/docs/Web/HTML/Element/Input/file
- 4: https://github.com/mdn/content/blob/main/files/en-us/web/html/reference/attributes/accept/index.md
Fix accept attribute to include leading dots for proper file filtering
The current implementation generates a comma-separated list of extensions without leading dots (e.g., pdf,docx), which browsers ignore because the HTML accept attribute strictly requires extensions to start with a period (e.g., .pdf, .docx). Update the value generation to prepend a dot to each extension:
Proposed fix
- accept={EVIDENCE_ALLOWED_EXTENSIONS.join(',')}
+ accept={EVIDENCE_ALLOWED_EXTENSIONS.map((e) => `.${e}`).join(',')}The runtime validation remains necessary, but this change ensures the native file picker correctly filters supported file types.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <FormFileInput | |
| id="evidence-file" | |
| label="File (optional)" | |
| onChange={handleFilechange} | |
| accept={EVIDENCE_ALLOWED_EXTENSIONS.join(',')} | |
| selectedFile={formData.file} | |
| error={fileError} | |
| touched={true} | |
| /> | |
| <FormFileInput | |
| id="evidence-file" | |
| label="File (optional)" | |
| onChange={handleFilechange} | |
| accept={EVIDENCE_ALLOWED_EXTENSIONS.map((e) => `.${e}`).join(',')} | |
| selectedFile={formData.file} | |
| error={fileError} | |
| touched={true} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/EvidenceForm.tsx` around lines 179 - 187, The file
picker filtering in the EvidenceForm component is using extension values without
the required leading dots, so the native accept filter is ineffective. Update
the accept value passed to FormFileInput in EvidenceForm to transform
EVIDENCE_ALLOWED_EXTENSIONS into dotted extensions before joining them, while
keeping the existing runtime validation and handleFilechange behavior unchanged.
| const ext = file.name.split('.').pop()?.toLowerCase() | ||
| if (!allowedExtensions.some((e) => e.toLowerCase() === ext)) { | ||
| return `File extension .${ext} is not supported. Allowed: ${allowedExtensions.map((e) => `.${e}`).join(', ')}` | ||
| } | ||
| return undefined | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Extension-less filenames yield a misleading error.
For a file named report (no dot), split('.').pop() returns the whole name report, so ext becomes "report" and the message reads File extension .report is not supported. Guard for the no-extension case to produce a clearer message.
🐛 Proposed guard
export const validateFileExtension = (
file: File,
allowedExtensions: string[]
): string | undefined => {
- const ext = file.name.split('.').pop()?.toLowerCase()
+ const parts = file.name.split('.')
+ const ext = parts.length > 1 ? parts.pop()?.toLowerCase() : undefined
if (!allowedExtensions.some((e) => e.toLowerCase() === ext)) {
- return `File extension .${ext} is not supported. Allowed: ${allowedExtensions.map((e) => `.${e}`).join(', ')}`
+ const allowed = allowedExtensions.map((e) => `.${e}`).join(', ')
+ return `File extension ${ext ? `.${ext}` : '(none)'} is not supported. Allowed: ${allowed}`
}
return undefined
}The same diff also resolves the SonarCloud nested-template-literal warning on Line 41 by extracting the allowed string.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const ext = file.name.split('.').pop()?.toLowerCase() | |
| if (!allowedExtensions.some((e) => e.toLowerCase() === ext)) { | |
| return `File extension .${ext} is not supported. Allowed: ${allowedExtensions.map((e) => `.${e}`).join(', ')}` | |
| } | |
| return undefined | |
| } | |
| const parts = file.name.split('.') | |
| const ext = parts.length > 1 ? parts.pop()?.toLowerCase() : undefined | |
| if (!allowedExtensions.some((e) => e.toLowerCase() === ext)) { | |
| const allowed = allowedExtensions.map((e) => `.${e}`).join(', ') | |
| return `File extension ${ext ? `.${ext}` : '(none)'} is not supported. Allowed: ${allowed}` | |
| } | |
| return undefined | |
| } |
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis
[warning] 41-41: Refactor this code to not use nested template literals.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/forms/shared/formValidationUtils.ts` around lines 39
- 44, The file extension check in formValidationUtils is treating extension-less
filenames as if the whole filename were the extension, which produces a
misleading message. Update the validation logic around the file.name split/pop
handling to explicitly detect when there is no dot in the name and return a
clearer no-extension error before comparing against allowedExtensions. While
touching this block, use a separate allowed string in the same validation helper
to avoid the nested template literal warning in the return message.
Source: Linters/SAST tools
| export type Claim = { | ||
| __typename?: string | ||
| id: string | ||
| createdAt?: string | ||
| description: string | ||
| hasEvidence?: boolean | ||
| key: string | ||
| name: string | ||
| order?: number | ||
| status: ClaimStatusEnum | ||
| updatedAt?: string | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Avoid loosening the generated GraphQL contract here.
This hand-written type marks createdAt, hasEvidence, order, and updatedAt as optional even though the backend node exposes them as concrete fields and the claims dashboard already consumes createdAt as present. That weakens type-checking right where this PR is adding cache updates and new routes. Prefer reusing the generated operation type (or make these fields required) so frontend and GraphQL stay in lockstep.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/types/claim.ts` around lines 3 - 14, The Claim type is weakening
the GraphQL contract by making backend-provided fields optional; update the
frontend type to stay aligned with the generated operation type instead of
hand-typing these fields. In Claim, make createdAt, hasEvidence, order, and
updatedAt required (or replace this alias with the generated GraphQL type used
by the claims dashboard and cache updates) so Claim, ClaimStatusEnum, and
related consumers remain in lockstep.
There was a problem hiding this comment.
14 issues found across 61 files
Confidence score: 2/5
frontend/package.jsondowngradesdompurifyfrom 3.4.11 to 3.4.9, which reintroduces a known sanitizer vulnerability; merging as-is reopens a concrete security risk in HTML sanitization paths—restore 3.4.11+ before merging.frontend/src/components/forms/shared/FormFileInput.tsxhas a behavior/accessibility mismatch (requiredis visual-only, and errors are not connected viaaria-describedby/aria-invalid), so users can submit without a file and assistive tech may miss validation feedback—wire the required/error props through to the native input before merge.- File/URL validation flow is inconsistent across
frontend/src/components/forms/shared/formValidationUtils.tsandfrontend/src/components/EvidenceForm.tsx(dotless filenames treated as extensions, andsourceUrlbackend errors can persist after file selection), which can leave users stuck in confusing or stale error states—fix extension parsing and clear the URL error when file selection satisfies the requirement. - A few interaction paths can fail confusingly in production: empty-action menus in
frontend/src/components/DropdownActions.tsx,NaNyearvariables infrontend/src/components/EvidenceActions.tsx, and query failures shown as “Access Denied” infrontend/src/app/board/[year]/candidates/[login]/claims/create/page.tsx—add guards and explicit error handling so failures are recoverable and correctly messaged before merging.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="frontend/src/components/DropdownActions.tsx">
<violation number="1" location="frontend/src/components/DropdownActions.tsx:20">
P3: New component duplicates existing dropdown behavior already implemented in `EntityActions`. Keeping both copies increases drift risk for future accessibility/keyboard fixes.</violation>
<violation number="2" location="frontend/src/components/DropdownActions.tsx:86">
P2: Component opens an empty dropdown when `options.length === 0`. Guarding empty options avoids a non-functional actions button and confusing keyboard/focus behavior.</violation>
</file>
<file name="frontend/src/app/board/[year]/candidates/[login]/claims/create/page.tsx">
<violation number="1" location="frontend/src/app/board/[year]/candidates/[login]/claims/create/page.tsx:50">
P2: Query failures are rendered as “Access Denied” because the access check runs even when `candidateQueryError` is set. This misclassifies backend/network errors and hides the real failure state.</violation>
</file>
<file name="frontend/src/components/ClaimForm.tsx">
<violation number="1" location="frontend/src/components/ClaimForm.tsx:41">
P3: This file duplicates shared form-state and submit-error logic already present in other forms. Extracting a shared hook/helper would reduce maintenance drift across forms.</violation>
</file>
<file name="frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/evidences/[evidenceKey]/page.tsx">
<violation number="1" location="frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/evidences/[evidenceKey]/page.tsx:102">
P2: Null file URL path is silent; download click can appear broken. Show an error toast when URL is missing.</violation>
</file>
<file name="frontend/src/server/mutations/claimMutations.ts">
<violation number="1" location="frontend/src/server/mutations/claimMutations.ts:10">
P2: Repeated field selections across all six mutations should use a shared GraphQL fragment to keep fields in sync and reduce maintenance burden.</violation>
</file>
<file name="frontend/src/components/EvidenceActions.tsx">
<violation number="1" location="frontend/src/components/EvidenceActions.tsx:63">
P2: Validate `year` before using it in mutation/query variables. Passing `NaN` into required `Int` variables can break evidence removal and cache updates.</violation>
<violation number="2" location="frontend/src/components/EvidenceActions.tsx:111">
P3: Use strict equality for status checks. Loose equality here is unnecessary and can cause coercion-based false positives.</violation>
</file>
<file name="frontend/src/components/forms/shared/FormFileInput.tsx">
<violation number="1" location="frontend/src/components/forms/shared/FormFileInput.tsx:12">
P2: `required` prop shows a visual asterisk but is not passed to `<input type="file">`. Browser won't enforce file selection, creating a mismatch between visual indication and actual behavior.</violation>
<violation number="2" location="frontend/src/components/forms/shared/FormFileInput.tsx:42">
P2: Error message not linked to input via `aria-describedby` or `aria-invalid`. Screen readers won't announce the error when focused on the file input.</violation>
</file>
<file name="frontend/src/components/forms/shared/formValidationUtils.ts">
<violation number="1" location="frontend/src/components/forms/shared/formValidationUtils.ts:39">
P2: `validateFileExtension` misidentifies files without a dot as having an extension. `split('.').pop()` returns the whole filename when no dot exists (e.g., 'Makefile'), producing a confusing error.</violation>
</file>
<file name="frontend/src/components/EvidenceForm.tsx">
<violation number="1" location="frontend/src/components/EvidenceForm.tsx:76">
P2: `sourceUrl` backend error is not cleared when a file is selected. Users can still see an invalid error state after satisfying the "file or URL" requirement.</violation>
<violation number="2" location="frontend/src/components/EvidenceForm.tsx:183">
P2: File picker `accept` string uses bare extensions instead of valid file type specifiers. This weakens client-side file type filtering in the chooser.</violation>
</file>
<file name="frontend/package.json">
<violation number="1" location="frontend/package.json:28">
P1: Downgrading dompurify from 3.4.11 to 3.4.9 reintroduces a security vulnerability. Version 3.4.11 fixes a leaky-config issue where `setConfig()` bypasses the `_parseConfig` clone-guard, allowing `uponSanitizeAttribute` hooks to permanently mutate the shared `ALLOWED_ATTR` object — an XSS bypass.</violation>
</file>
Note: This PR contains a large number of files. cubic only reviews up to 40 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.
Re-trigger cubic
| "date-fns": "4.3.0", | ||
| "dayjs": "1.11.21", | ||
| "dompurify": "3.4.11", | ||
| "dompurify": "3.4.9", |
There was a problem hiding this comment.
P1: Downgrading dompurify from 3.4.11 to 3.4.9 reintroduces a security vulnerability. Version 3.4.11 fixes a leaky-config issue where setConfig() bypasses the _parseConfig clone-guard, allowing uponSanitizeAttribute hooks to permanently mutate the shared ALLOWED_ATTR object — an XSS bypass.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/package.json, line 28:
<comment>Downgrading dompurify from 3.4.11 to 3.4.9 reintroduces a security vulnerability. Version 3.4.11 fixes a leaky-config issue where `setConfig()` bypasses the `_parseConfig` clone-guard, allowing `uponSanitizeAttribute` hooks to permanently mutate the shared `ALLOWED_ATTR` object — an XSS bypass.</comment>
<file context>
@@ -17,18 +17,19 @@
"date-fns": "4.3.0",
"dayjs": "1.11.21",
- "dompurify": "3.4.11",
+ "dompurify": "3.4.9",
"framer-motion": "12.40.0",
"graphql": "16.14.0",
</file context>
| "dompurify": "3.4.9", | |
| "dompurify": "3.4.11", |
| } | ||
| } | ||
|
|
||
| return ( |
There was a problem hiding this comment.
P2: Component opens an empty dropdown when options.length === 0. Guarding empty options avoids a non-functional actions button and confusing keyboard/focus behavior.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/DropdownActions.tsx, line 86:
<comment>Component opens an empty dropdown when `options.length === 0`. Guarding empty options avoids a non-functional actions button and confusing keyboard/focus behavior.</comment>
<file context>
@@ -0,0 +1,139 @@
+ }
+ }
+
+ return (
+ <div className="relative" ref={dropdownRef}>
+ <button
</file context>
|
|
||
| const isCandidate = candidateGraphQLData?.boardOfDirectors?.candidate != null | ||
|
|
||
| if (!isCandidate || session?.user?.login !== login) { |
There was a problem hiding this comment.
P2: Query failures are rendered as “Access Denied” because the access check runs even when candidateQueryError is set. This misclassifies backend/network errors and hides the real failure state.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/app/board/[year]/candidates/[login]/claims/create/page.tsx, line 50:
<comment>Query failures are rendered as “Access Denied” because the access check runs even when `candidateQueryError` is set. This misclassifies backend/network errors and hides the real failure state.</comment>
<file context>
@@ -0,0 +1,127 @@
+
+ const isCandidate = candidateGraphQLData?.boardOfDirectors?.candidate != null
+
+ if (!isCandidate || session?.user?.login !== login) {
+ return (
+ <AccessDeniedDisplay
</file context>
| }) | ||
| return | ||
| } | ||
| if (url) { |
There was a problem hiding this comment.
P2: Null file URL path is silent; download click can appear broken. Show an error toast when URL is missing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/app/board/[year]/candidates/[login]/claims/[claimKey]/evidences/[evidenceKey]/page.tsx, line 102:
<comment>Null file URL path is silent; download click can appear broken. Show an error toast when URL is missing.</comment>
<file context>
@@ -0,0 +1,134 @@
+ })
+ return
+ }
+ if (url) {
+ const a = document.createElement('a')
+ a.href = url
</file context>
| @@ -0,0 +1,127 @@ | |||
| import { gql } from '@apollo/client' | |||
There was a problem hiding this comment.
P2: Repeated field selections across all six mutations should use a shared GraphQL fragment to keep fields in sync and reduce maintenance burden.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/server/mutations/claimMutations.ts, line 10:
<comment>Repeated field selections across all six mutations should use a shared GraphQL fragment to keep fields in sync and reduce maintenance burden.</comment>
<file context>
@@ -0,0 +1,127 @@
+ code
+ message
+ claim {
+ createdAt
+ description
+ hasEvidence
</file context>
| id="evidence-file" | ||
| label="File (optional)" | ||
| onChange={handleFilechange} | ||
| accept={EVIDENCE_ALLOWED_EXTENSIONS.join(',')} |
There was a problem hiding this comment.
P2: File picker accept string uses bare extensions instead of valid file type specifiers. This weakens client-side file type filtering in the chooser.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/EvidenceForm.tsx, line 183:
<comment>File picker `accept` string uses bare extensions instead of valid file type specifiers. This weakens client-side file type filtering in the chooser.</comment>
<file context>
@@ -0,0 +1,195 @@
+ id="evidence-file"
+ label="File (optional)"
+ onChange={handleFilechange}
+ accept={EVIDENCE_ALLOWED_EXTENSIONS.join(',')}
+ selectedFile={formData.file}
+ error={fileError}
</file context>
| } else { | ||
| setFileError(undefined) | ||
| } | ||
| if (backendErrors['file']) { |
There was a problem hiding this comment.
P2: sourceUrl backend error is not cleared when a file is selected. Users can still see an invalid error state after satisfying the "file or URL" requirement.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/EvidenceForm.tsx, line 76:
<comment>`sourceUrl` backend error is not cleared when a file is selected. Users can still see an invalid error state after satisfying the "file or URL" requirement.</comment>
<file context>
@@ -0,0 +1,195 @@
+ } else {
+ setFileError(undefined)
+ }
+ if (backendErrors['file']) {
+ setBackendErrors((prev) => {
+ const next = { ...prev }
</file context>
| } | ||
|
|
||
| const DropdownActions: React.FC<DropdownActionsProps> = ({ options, label }) => { | ||
| const [dropdownOpen, setDropdownOpen] = useState(false) |
There was a problem hiding this comment.
P3: New component duplicates existing dropdown behavior already implemented in EntityActions. Keeping both copies increases drift risk for future accessibility/keyboard fixes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/DropdownActions.tsx, line 20:
<comment>New component duplicates existing dropdown behavior already implemented in `EntityActions`. Keeping both copies increases drift risk for future accessibility/keyboard fixes.</comment>
<file context>
@@ -0,0 +1,139 @@
+}
+
+const DropdownActions: React.FC<DropdownActionsProps> = ({ options, label }) => {
+ const [dropdownOpen, setDropdownOpen] = useState(false)
+ const [focusIndex, setFocusIndex] = useState(-1)
+ const dropdownRef = useRef<HTMLDivElement>(null)
</file context>
| const [touched, setTouched] = useState<Record<string, boolean>>({}) | ||
| const [backendErrors, setBackendErrors] = useState<Record<string, string>>({}) | ||
|
|
||
| const handleInputChange = (name: string, value: string | number) => { |
There was a problem hiding this comment.
P3: This file duplicates shared form-state and submit-error logic already present in other forms. Extracting a shared hook/helper would reduce maintenance drift across forms.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/ClaimForm.tsx, line 41:
<comment>This file duplicates shared form-state and submit-error logic already present in other forms. Extracting a shared hook/helper would reduce maintenance drift across forms.</comment>
<file context>
@@ -0,0 +1,134 @@
+ const [touched, setTouched] = useState<Record<string, boolean>>({})
+ const [backendErrors, setBackendErrors] = useState<Record<string, string>>({})
+
+ const handleInputChange = (name: string, value: string | number) => {
+ setFormData((prev) => ({ ...prev, [name]: value }))
+ if (backendErrors[name]) {
</file context>
| } | ||
|
|
||
| const options = [ | ||
| ...(claim.status == 'DRAFT' |
There was a problem hiding this comment.
P3: Use strict equality for status checks. Loose equality here is unnecessary and can cause coercion-based false positives.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/EvidenceActions.tsx, line 111:
<comment>Use strict equality for status checks. Loose equality here is unnecessary and can cause coercion-based false positives.</comment>
<file context>
@@ -0,0 +1,183 @@
+ }
+
+ const options = [
+ ...(claim.status == 'DRAFT'
+ ? [
+ {
</file context>



Proposed change
Resolves #5004
PR Depends on #5006 and needs to be rebased.
TODO: update description.
Checklist