Skip to content

E2609: Calibration tab UI & comparison report#177

Open
ruju4a wants to merge 15 commits into
expertiza:mainfrom
ruju4a:main
Open

E2609: Calibration tab UI & comparison report#177
ruju4a wants to merge 15 commits into
expertiza:mainfrom
ruju4a:main

Conversation

@ruju4a
Copy link
Copy Markdown

@ruju4a ruju4a commented Apr 27, 2026

Calibration tab (assignment editor)

  • Search box to add participants by username
  • Table with participant name, review status (Begin / View | Edit), View review report link, submitted content (hyperlinks + files), and Remove action

Calibration report page (CalibrationReview)

  • Fetches report JSON from GET /assignments/:id/reports/calibration/:map_id
  • Class comparison tabCalibrationStackedChart (Recharts stacked bars: agree / near / disagree per rubric item)
  • Rubric detail tabCalibrationRubricDetailPanel; per-student dropdown; per-item score diff cards with instructor vs student score, comments, and mini class-distribution chart

Utilities

  • calibrationReportNormalize.ts — transforms raw report JSON into chart-ready structures

Code hygiene

  • Demo logic extracted into useCalibrationInstructorDemo (gitignored), keeping production AssignmentEditor clean

Summary by CodeRabbit

  • New Features

    • New calibration review page with comparison and rubric-detail tabs, plus a route to access it
    • Calibration tab: backend-driven participant list with add/remove by username, Begin action, and view/edit for completed reviews
    • Visuals: stacked class comparison chart and per-item distribution charts; submitted content links/files shown
  • Tests

    • Added comprehensive unit and integration tests and test setup to support calibration UI components

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

Adds an assignment calibration feature: new route and page for calibration review, backend-driven participant management in the AssignmentEditor, normalization utilities, visualization components (stacked and distribution charts), a rubric detail panel, supporting hook for instructor demo, and associated tests and test setup changes.

Changes

Cohort / File(s) Summary
Routing & Page
src/App.tsx, src/pages/Assignments/CalibrationReview.tsx
Registers a new route assignments/edit/:assignmentId/calibration/:mapId and adds CalibrationReview page that fetches a calibration report, manages loading/error state, normalizes data, and renders comparison and rubric tabs including submitted content.
Assignment Editor (participant flows)
src/pages/Assignments/AssignmentEditor.tsx
Replaces placeholder calibration rows with backend-driven fetch of calibration participants; adds POST (add by username) and DELETE (remove participant) mutations, refresh-on-success behavior, Begin demo action integration, and defensive submitted-content rendering.
Normalization & Types
src/pages/Assignments/calibrationReportNormalize.ts
New exported normalizeCalibrationReport and related TypeScript interfaces/types to transform raw report responses into UI-ready structures (stacked chart rows, reviewer options, rubric detail rows, agreement buckets).
Charts & Panels
src/pages/Assignments/components/CalibrationStackedChart.tsx, src/pages/Assignments/components/CalibrationRubricDetailPanel.tsx, src/pages/Assignments/components/CalibrationRubricDistributionChart.tsx
Adds three visualization components: stacked class-comparison chart, rubric detail panel with reviewer selection and per-item cards, and per-item distribution chart (BarChart) with test-friendly data-testid hooks.
Instructor Demo Hook
src/pages/Assignments/hooks/useCalibrationInstructorDemo.ts
New hook exposing handleBeginCalibrationReview(mapId) which POSTs mock instructor responses and emits success/danger alerts; calls provided onSuccess callback on success.
Tests
src/pages/Assignments/__tests__/CalibrationReview.test.tsx, src/pages/Assignments/__tests__/CalibrationRubricDetailPanel.test.tsx, src/pages/Assignments/__tests__/CalibrationStackedChart.test.tsx, src/pages/Assignments/__tests__/calibrationReportNormalize.test.ts
Adds unit and integration tests covering normalization logic, stacked chart rendering, rubric detail panel behavior (including reviewer switching), and CalibrationReview page loading/error flows with mocked API responses.
Test Setup
src/test/setup.ts
Adds a no-op ResizeObserver mock on window/globalThis to support charting/rendering in tests.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser
    participant Backend
    participant NormalizeFn as normalizeCalibrationReport()
    participant CalibrationReview as CalibrationReview Component
    participant Charts as Chart Components

    User->>Browser: Navigate to calibration review route
    Browser->>Backend: GET /assignments/{id}/reports/calibration/{mapId}
    Backend-->>Browser: CalibrationReportResponse
    Browser->>NormalizeFn: normalizeCalibrationReport(response)
    NormalizeFn-->>Browser: NormalizedCalibrationReport
    Browser->>CalibrationReview: set state (report, selectedReviewerId)
    CalibrationReview->>Charts: render comparison + rubric components
    Charts-->>Browser: display charts and rubric detail
    User->>CalibrationReview: change reviewer selection
    CalibrationReview->>Charts: re-render with reviewer-specific rows
    Charts-->>Browser: update displayed rubric details
Loading
sequenceDiagram
    participant User
    participant AssignmentEditor as AssignmentEditor Component
    participant Backend
    participant ParticipantTable as Participant Table

    User->>AssignmentEditor: Enter username + click Add (or press Enter)
    AssignmentEditor->>Backend: POST /assignments/:id/review_mappings/calibration_participants { username }
    Backend-->>AssignmentEditor: 2xx success
    AssignmentEditor->>AssignmentEditor: show success alert
    AssignmentEditor->>Backend: GET /assignments/:id/review_mappings/calibration_participants (refresh)
    Backend-->>AssignmentEditor: updated participants list
    AssignmentEditor->>ParticipantTable: update rows state
    ParticipantTable-->>User: show new participant row

    User->>ParticipantTable: Click "Remove" on a row
    ParticipantTable->>Backend: DELETE /assignments/:id/review_mappings/calibration_participants/{participant_id}
    Backend-->>ParticipantTable: success
    ParticipantTable->>AssignmentEditor: trigger refresh
    AssignmentEditor->>ParticipantTable: update rows state

    User->>ParticipantTable: Click "Begin" on not_started row
    ParticipantTable->>Backend: POST mock instructor endpoint via hook
    Backend-->>ParticipantTable: success
    ParticipantTable->>AssignmentEditor: refresh participants
    AssignmentEditor->>ParticipantTable: show updated status (View/Edit)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hop! I found the calibration stream,
Charts that sparkle, reviewers that gleam,
Participants added, removed with a tap,
Normalized data snug in my lap,
Hooray — code carrots for the team!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'E2609: Calibration tab UI & comparison report' directly addresses the main changes: calibration tab UI and comparison report functionality added across multiple components.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@ruju4a ruju4a changed the title E2609: Review Calibration E2609: Calibration tab UI & comparison report Apr 27, 2026
@ruju4a ruju4a marked this pull request as ready for review April 27, 2026 19:16
Integrate calibration tab and report page changes
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
src/pages/Assignments/components/CalibrationRubricDistributionChart.tsx (1)

24-25: Consider making the chart container width responsive.

A fixed width: 300 can cause overflow in smaller containers. width: "100%" with maxWidth: 300 preserves layout better.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/components/CalibrationRubricDistributionChart.tsx`
around lines 24 - 25, The chart container in CalibrationRubricDistributionChart
currently uses a fixed width (the outer div with data-testid
`rubric-distribution-${row.itemId}`); change its inline style from width: 300 to
a responsive style like width: "100%" and maxWidth: 300 so the chart scales down
in smaller containers while preserving max width. Update the outer div's style
object accordingly (keep the existing data-testid and inner height div
unchanged).
src/pages/Assignments/__tests__/CalibrationStackedChart.test.tsx (1)

45-46: Make numeric assertions less brittle.

getByText("4") / getByText("5") can become ambiguous as UI grows. Prefer scoping to the instructor table (e.g., within(...)) or asserting full row content.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/__tests__/CalibrationStackedChart.test.tsx` around
lines 45 - 46, The numeric assertions using screen.getByText("4") and
screen.getByText("5") are brittle and can match unrelated elements; scope them
to the instructor table or assert the full row content instead: locate the
instructor table (e.g., using screen.getByRole('table', { name: /instructor/i })
or a container query), then use within(table).getByText(...) or
within(row).toHaveTextContent('Instructor Name 4 5') to assert the exact row
content; replace the two screen.getByText calls with scoped within(...) lookups
for the instructor table/row to make the test robust.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/Assignments/AssignmentEditor.tsx`:
- Around line 583-587: In AssignmentEditor.tsx inside the rows.map block (where
personName is computed), stop prefixing the fallback with "Team_": change the
participant_name assignment from row.team_name || `Team_${personName}` to use
the real participant name as the fallback (e.g., row.team_name || personName) so
that when team_name is absent the UI shows the actual username/handle/full_name
used in searches and actions.
- Around line 1308-1315: The Add and Remove calibration Buttons in
AssignmentEditor.tsx are missing an explicit type and will default to submitting
the Formik form; update the <Button> for handleAddCalibrationParticipant (the
one using calibrationUsername and disabled={!calibrationUsername.trim()}) and
any corresponding calibration Remove button to include type="button" so clicks
don't trigger form submit; locate the Button components in the AssignmentEditor
component and add the type prop to each calibration action Button.

In `@src/pages/Assignments/calibrationReportNormalize.ts`:
- Around line 101-116: latestStudentResponsesForMaps is currently choosing the
newest response even if it's an autosaved draft; update the selection logic to
ignore drafts by only considering responses with is_submitted === true (e.g.,
filter responses to responses.filter(r => r.is_submitted) before the reduce, or
add a guard inside the reduce to skip when response.is_submitted is false) while
keeping the same reduce-by-map and the existing sort by reviewer_name so
submitted calibrations replace drafts in the reviewer dropdown and rubric detail
panel.

In `@src/pages/Assignments/CalibrationReview.tsx`:
- Around line 56-58: The conditional that chooses reviewer-specific rows
incorrectly treats a reviewer id of 0 as falsy; update the check in the
expression that uses normalizedReport and selectedReviewerId so it uses an
explicit null check (selectedReviewerId !== null) instead of a truthy check,
keeping the same fallback to normalizedReport.rubricDetailRows when the
reviewer-specific map
(normalizedReport.rubricDetailRowsByReviewer[selectedReviewerId]) is missing;
modify the expression referencing normalizedReport, selectedReviewerId,
rubricDetailRowsByReviewer, and rubricDetailRows accordingly.
- Around line 20-43: The useEffect's loadReport can have out-of-order responses
that overwrite state; prevent this by tracking and validating the active request
before calling setReport/setError/setLoading: create a per-effect signal (either
an AbortController passed into axiosClient.get or an incrementing requestId
stored in a ref) inside the useEffect, attach it to the axios request, and in
the response and catch handlers verify the signal matches (or that
!signal.aborted) before calling setReport, setError or setLoading; ensure you
abort/mark the signal stale in the cleanup return of the useEffect so previous
requests cannot update state (update code in the useEffect surrounding
loadReport, and references to loadReport, axiosClient.get, setReport, setError,
setLoading).
- Around line 127-131: In CalibrationReview.tsx, the hyperlinks.map render is
using backend-provided links directly; add a validation step (e.g., create a
helper validateUrl or isSafeProtocol) that uses the URL constructor or regex to
allow only safe protocols (http:, https:, mailto:) and rejects others; update
the mapping in the CalibrationReview component to either filter out unsafe links
or render them with a safe fallback (e.g., plain text or href="#" and no target)
when validateUrl(link) returns false, ensuring the anchor tag's href is never
set to an unsafe scheme.

---

Nitpick comments:
In `@src/pages/Assignments/__tests__/CalibrationStackedChart.test.tsx`:
- Around line 45-46: The numeric assertions using screen.getByText("4") and
screen.getByText("5") are brittle and can match unrelated elements; scope them
to the instructor table or assert the full row content instead: locate the
instructor table (e.g., using screen.getByRole('table', { name: /instructor/i })
or a container query), then use within(table).getByText(...) or
within(row).toHaveTextContent('Instructor Name 4 5') to assert the exact row
content; replace the two screen.getByText calls with scoped within(...) lookups
for the instructor table/row to make the test robust.

In `@src/pages/Assignments/components/CalibrationRubricDistributionChart.tsx`:
- Around line 24-25: The chart container in CalibrationRubricDistributionChart
currently uses a fixed width (the outer div with data-testid
`rubric-distribution-${row.itemId}`); change its inline style from width: 300 to
a responsive style like width: "100%" and maxWidth: 300 so the chart scales down
in smaller containers while preserving max width. Update the outer div's style
object accordingly (keep the existing data-testid and inner height div
unchanged).
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0458fe60-ab79-48cc-bf73-b12289bdbedc

📥 Commits

Reviewing files that changed from the base of the PR and between 474e9f7 and 205b2da.

📒 Files selected for processing (11)
  • src/App.tsx
  • src/pages/Assignments/AssignmentEditor.tsx
  • src/pages/Assignments/CalibrationReview.tsx
  • src/pages/Assignments/__tests__/CalibrationRubricDetailPanel.test.tsx
  • src/pages/Assignments/__tests__/CalibrationStackedChart.test.tsx
  • src/pages/Assignments/__tests__/calibrationReportNormalize.test.ts
  • src/pages/Assignments/calibrationReportNormalize.ts
  • src/pages/Assignments/components/CalibrationRubricDetailPanel.tsx
  • src/pages/Assignments/components/CalibrationRubricDistributionChart.tsx
  • src/pages/Assignments/components/CalibrationStackedChart.tsx
  • src/test/setup.ts

Comment on lines +583 to +587
rows.map((row: any) => {
const personName = row.full_name || row.username || row.handle || `Participant ${row.participant_id}`;
return {
participant_id: row.participant_id,
participant_name: row.team_name || `Team_${personName}`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use the real participant name as the fallback.

The column is labeled “Participant name”, but this fallback turns alice into Team_alice whenever team_name is absent. That makes the list diverge from the searched username and is confusing in remove/report actions.

Suggested fix
-            participant_name: row.team_name || `Team_${personName}`,
+            participant_name: row.team_name || personName,
📝 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.

Suggested change
rows.map((row: any) => {
const personName = row.full_name || row.username || row.handle || `Participant ${row.participant_id}`;
return {
participant_id: row.participant_id,
participant_name: row.team_name || `Team_${personName}`,
rows.map((row: any) => {
const personName = row.full_name || row.username || row.handle || `Participant ${row.participant_id}`;
return {
participant_id: row.participant_id,
participant_name: row.team_name || personName,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/AssignmentEditor.tsx` around lines 583 - 587, In
AssignmentEditor.tsx inside the rows.map block (where personName is computed),
stop prefixing the fallback with "Team_": change the participant_name assignment
from row.team_name || `Team_${personName}` to use the real participant name as
the fallback (e.g., row.team_name || personName) so that when team_name is
absent the UI shows the actual username/handle/full_name used in searches and
actions.

Comment on lines +1308 to +1315
<Button
variant="outline-secondary"
onClick={handleAddCalibrationParticipant}
disabled={!calibrationUsername.trim()}
style={{ height: 38, lineHeight: 1, padding: '0 1rem', boxSizing: 'border-box' }}
>
Add
</Button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l src/pages/Assignments/AssignmentEditor.tsx

Repository: expertiza/reimplementation-front-end

Length of output: 128


🏁 Script executed:

# Check the code around lines 1308-1315
sed -n '1295,1320p' src/pages/Assignments/AssignmentEditor.tsx

Repository: expertiza/reimplementation-front-end

Length of output: 1901


🏁 Script executed:

# Check the code around lines 1437-1443
sed -n '1425,1450p' src/pages/Assignments/AssignmentEditor.tsx

Repository: expertiza/reimplementation-front-end

Length of output: 2211


🏁 Script executed:

# Find the Form wrapper context - search for Formik Form
rg "from.*formik|import.*Formik|<Form" src/pages/Assignments/AssignmentEditor.tsx -A 2 -B 2 | head -50

Repository: expertiza/reimplementation-front-end

Length of output: 2795


🏁 Script executed:

# Check Button component import and implementation
rg "Button" src/pages/Assignments/AssignmentEditor.tsx | head -5

Repository: expertiza/reimplementation-front-end

Length of output: 417


🏁 Script executed:

rg "handleAddCalibrationParticipant" src/pages/Assignments/AssignmentEditor.tsx -B 5 -A 5

Repository: expertiza/reimplementation-front-end

Length of output: 1590


🏁 Script executed:

rg "variant=\"outline-secondary\"" src/pages/Assignments/AssignmentEditor.tsx -B 3 -A 3 | head -100

Repository: expertiza/reimplementation-front-end

Length of output: 3226


🏁 Script executed:

# Search for the exact Button code around line 1308-1315
sed -n '1300,1325p' src/pages/Assignments/AssignmentEditor.tsx

Repository: expertiza/reimplementation-front-end

Length of output: 1838


🏁 Script executed:

rg "handleRemoveCalibrationParticipant" src/pages/Assignments/AssignmentEditor.tsx -B 3 -A 3

Repository: expertiza/reimplementation-front-end

Length of output: 732


🏁 Script executed:

# Check the exact lines 1437-1443
sed -n '1430,1450p' src/pages/Assignments/AssignmentEditor.tsx

Repository: expertiza/reimplementation-front-end

Length of output: 1793


🏁 Script executed:

# Search for both Button components to verify if they're inside Form context
sed -n '1270,1450p' src/pages/Assignments/AssignmentEditor.tsx | grep -A 200 "Calibration"

Repository: expertiza/reimplementation-front-end

Length of output: 11128


🏁 Script executed:

# Check the overall structure to confirm Form context
sed -n '1200,1330p' src/pages/Assignments/AssignmentEditor.tsx | grep -E "<Form>|<Formik|<Tab|calibration" | head -20

Repository: expertiza/reimplementation-front-end

Length of output: 1379


🏁 Script executed:

# Verify the second button location and form context
sed -n '1380,1460p' src/pages/Assignments/AssignmentEditor.tsx | grep -E "<Form>|</Form>|<Tab|variant=" | head -30

Repository: expertiza/reimplementation-front-end

Length of output: 254


🌐 Web query:

react-bootstrap Button component default type attribute behavior

💡 Result:

The react-bootstrap Button component does not set a default value for the HTML 'type' attribute. When used as a element (the default), it renders without an explicit type="...", so browsers default to type="submit" if the Button is inside a element. This can cause unintended form submissions for non-submit buttons. To avoid this, explicitly set type="button" (or "submit", "reset" as needed) on the Button component. This behavior comes from the Button source code, which spreads all props (including 'type' if provided) to the underlying element via useButtonProps from @restart/ui and passes ...props. No default type is set in ButtonProps or the implementation. Official docs do not document a default type, and examples omit it.

Citations:


Add type="button" to calibration action buttons inside the Formik Form.

Both the Add and Remove calibration buttons currently lack an explicit type attribute. Since react-bootstrap's Button component does not set a default type, browsers will treat these as type="submit" when inside a form element, risking unintended assignment save submissions when users click Add/Remove.

Suggested fix
                       <Button
+                        type="button"
                         variant="outline-secondary"
                         onClick={handleAddCalibrationParticipant}
                         disabled={!calibrationUsername.trim()}
                         style={{ height: 38, lineHeight: 1, padding: '0 1rem', boxSizing: 'border-box' }}
                       >
                         Add
                       </Button>
                               <Button
+                                type="button"
                                 variant="outline-danger"
                                 size="sm"
                                 onClick={() => handleRemoveCalibrationParticipant(row.original.participant_id)}
                               >
                                 Remove
                               </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/AssignmentEditor.tsx` around lines 1308 - 1315, The Add
and Remove calibration Buttons in AssignmentEditor.tsx are missing an explicit
type and will default to submitting the Formik form; update the <Button> for
handleAddCalibrationParticipant (the one using calibrationUsername and
disabled={!calibrationUsername.trim()}) and any corresponding calibration Remove
button to include type="button" so clicks don't trigger form submit; locate the
Button components in the AssignmentEditor component and add the type prop to
each calibration action Button.

Comment on lines +101 to +116
const latestStudentResponsesForMaps = (responses: CalibrationResponse[]) =>
Object.values(
responses.reduce<Record<number, CalibrationResponse>>((latestByMap, response) => {
const current = latestByMap[response.map_id];

if (!current || new Date(response.updated_at).getTime() > new Date(current.updated_at).getTime()) {
latestByMap[response.map_id] = response;
}

return latestByMap;
}, {})
).sort((left, right) => {
const leftName = left.reviewer_name ?? "";
const rightName = right.reviewer_name ?? "";
return leftName.localeCompare(rightName);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Exclude drafts when selecting the latest student response.

latestStudentResponsesForMaps currently picks the newest record even when is_submitted is false. A newer autosaved draft would then replace the actual submitted calibration in the reviewer dropdown and rubric detail panel.

Suggested fix
 const latestStudentResponsesForMaps = (responses: CalibrationResponse[]) =>
   Object.values(
-    responses.reduce<Record<number, CalibrationResponse>>((latestByMap, response) => {
+    responses
+      .filter((response) => response.is_submitted)
+      .reduce<Record<number, CalibrationResponse>>((latestByMap, response) => {
       const current = latestByMap[response.map_id];
 
       if (!current || new Date(response.updated_at).getTime() > new Date(current.updated_at).getTime()) {
         latestByMap[response.map_id] = response;
       }
 
       return latestByMap;
-    }, {})
+    }, {})
   ).sort((left, right) => {
📝 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.

Suggested change
const latestStudentResponsesForMaps = (responses: CalibrationResponse[]) =>
Object.values(
responses.reduce<Record<number, CalibrationResponse>>((latestByMap, response) => {
const current = latestByMap[response.map_id];
if (!current || new Date(response.updated_at).getTime() > new Date(current.updated_at).getTime()) {
latestByMap[response.map_id] = response;
}
return latestByMap;
}, {})
).sort((left, right) => {
const leftName = left.reviewer_name ?? "";
const rightName = right.reviewer_name ?? "";
return leftName.localeCompare(rightName);
});
const latestStudentResponsesForMaps = (responses: CalibrationResponse[]) =>
Object.values(
responses
.filter((response) => response.is_submitted)
.reduce<Record<number, CalibrationResponse>>((latestByMap, response) => {
const current = latestByMap[response.map_id];
if (!current || new Date(response.updated_at).getTime() > new Date(current.updated_at).getTime()) {
latestByMap[response.map_id] = response;
}
return latestByMap;
}, {})
).sort((left, right) => {
const leftName = left.reviewer_name ?? "";
const rightName = right.reviewer_name ?? "";
return leftName.localeCompare(rightName);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/calibrationReportNormalize.ts` around lines 101 - 116,
latestStudentResponsesForMaps is currently choosing the newest response even if
it's an autosaved draft; update the selection logic to ignore drafts by only
considering responses with is_submitted === true (e.g., filter responses to
responses.filter(r => r.is_submitted) before the reduce, or add a guard inside
the reduce to skip when response.is_submitted is false) while keeping the same
reduce-by-map and the existing sort by reviewer_name so submitted calibrations
replace drafts in the reviewer dropdown and rubric detail panel.

Comment on lines +20 to +43
useEffect(() => {
const loadReport = async () => {
if (!assignmentId || !mapId) {
setError("Missing assignment or calibration map id.");
setLoading(false);
return;
}

try {
setLoading(true);
setError(null);
const response = await axiosClient.get<CalibrationReportResponse>(
`/assignments/${assignmentId}/reports/calibration/${mapId}`
);
setReport(response.data);
} catch (err: any) {
setError(err?.response?.data?.error || "Unable to load calibration report");
} finally {
setLoading(false);
}
};

loadReport();
}, [assignmentId, mapId]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent stale state from out-of-order fetch completions.

If route params change quickly, an older request can resolve last and overwrite the newer report/error state.

🔧 Suggested fix
 useEffect(() => {
+  let isCurrent = true;
   const loadReport = async () => {
     if (!assignmentId || !mapId) {
-      setError("Missing assignment or calibration map id.");
-      setLoading(false);
+      if (isCurrent) {
+        setError("Missing assignment or calibration map id.");
+        setLoading(false);
+      }
       return;
     }

     try {
-      setLoading(true);
-      setError(null);
+      if (isCurrent) {
+        setLoading(true);
+        setError(null);
+      }
       const response = await axiosClient.get<CalibrationReportResponse>(
         `/assignments/${assignmentId}/reports/calibration/${mapId}`
       );
-      setReport(response.data);
+      if (isCurrent) {
+        setReport(response.data);
+      }
     } catch (err: any) {
-      setError(err?.response?.data?.error || "Unable to load calibration report");
+      if (isCurrent) {
+        setError(err?.response?.data?.error || "Unable to load calibration report");
+      }
     } finally {
-      setLoading(false);
+      if (isCurrent) {
+        setLoading(false);
+      }
     }
   };

   loadReport();
+  return () => {
+    isCurrent = false;
+  };
 }, [assignmentId, mapId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/CalibrationReview.tsx` around lines 20 - 43, The
useEffect's loadReport can have out-of-order responses that overwrite state;
prevent this by tracking and validating the active request before calling
setReport/setError/setLoading: create a per-effect signal (either an
AbortController passed into axiosClient.get or an incrementing requestId stored
in a ref) inside the useEffect, attach it to the axios request, and in the
response and catch handlers verify the signal matches (or that !signal.aborted)
before calling setReport, setError or setLoading; ensure you abort/mark the
signal stale in the cleanup return of the useEffect so previous requests cannot
update state (update code in the useEffect surrounding loadReport, and
references to loadReport, axiosClient.get, setReport, setError, setLoading).

Comment on lines +56 to +58
normalizedReport && selectedReviewerId
? normalizedReport.rubricDetailRowsByReviewer[selectedReviewerId] ?? normalizedReport.rubricDetailRows
: normalizedReport?.rubricDetailRows ?? [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use an explicit null check for selectedReviewerId.

The current truthy check skips reviewer-specific rows when id is 0. Use selectedReviewerId !== null for correctness.

🔧 Suggested fix
 const rubricDetailRows =
-  normalizedReport && selectedReviewerId
+  normalizedReport && selectedReviewerId !== null
     ? normalizedReport.rubricDetailRowsByReviewer[selectedReviewerId] ?? normalizedReport.rubricDetailRows
     : normalizedReport?.rubricDetailRows ?? [];
📝 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.

Suggested change
normalizedReport && selectedReviewerId
? normalizedReport.rubricDetailRowsByReviewer[selectedReviewerId] ?? normalizedReport.rubricDetailRows
: normalizedReport?.rubricDetailRows ?? [];
const rubricDetailRows =
normalizedReport && selectedReviewerId !== null
? normalizedReport.rubricDetailRowsByReviewer[selectedReviewerId] ?? normalizedReport.rubricDetailRows
: normalizedReport?.rubricDetailRows ?? [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/CalibrationReview.tsx` around lines 56 - 58, The
conditional that chooses reviewer-specific rows incorrectly treats a reviewer id
of 0 as falsy; update the check in the expression that uses normalizedReport and
selectedReviewerId so it uses an explicit null check (selectedReviewerId !==
null) instead of a truthy check, keeping the same fallback to
normalizedReport.rubricDetailRows when the reviewer-specific map
(normalizedReport.rubricDetailRowsByReviewer[selectedReviewerId]) is missing;
modify the expression referencing normalizedReport, selectedReviewerId,
rubricDetailRowsByReviewer, and rubricDetailRows accordingly.

Comment on lines +127 to +131
{hyperlinks.map((link, idx) => (
<li key={`${link}-${idx}`}>
<a href={link} target="_blank" rel="noopener noreferrer">
{link}
</a>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate submitted hyperlink protocols before rendering anchor tags.

Rendering backend-provided links directly allows unsafe schemes (e.g., javascript:), which is a security risk on click.

🔒 Suggested fix
 {hyperlinks.map((link, idx) => (
-  <li key={`${link}-${idx}`}>
-    <a href={link} target="_blank" rel="noopener noreferrer">
-      {link}
-    </a>
-  </li>
+  <li key={`${link}-${idx}`}>
+    {(() => {
+      try {
+        const parsed = new URL(link);
+        const isSafe = parsed.protocol === "http:" || parsed.protocol === "https:";
+        return isSafe ? (
+          <a href={parsed.toString()} target="_blank" rel="noopener noreferrer">
+            {link}
+          </a>
+        ) : (
+          <span>{link}</span>
+        );
+      } catch {
+        return <span>{link}</span>;
+      }
+    })()}
+  </li>
 ))}
📝 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.

Suggested change
{hyperlinks.map((link, idx) => (
<li key={`${link}-${idx}`}>
<a href={link} target="_blank" rel="noopener noreferrer">
{link}
</a>
{hyperlinks.map((link, idx) => (
<li key={`${link}-${idx}`}>
{(() => {
try {
const parsed = new URL(link);
const isSafe = parsed.protocol === "http:" || parsed.protocol === "https:";
return isSafe ? (
<a href={parsed.toString()} target="_blank" rel="noopener noreferrer">
{link}
</a>
) : (
<span>{link}</span>
);
} catch {
return <span>{link}</span>;
}
})()}
</li>
))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/CalibrationReview.tsx` around lines 127 - 131, In
CalibrationReview.tsx, the hyperlinks.map render is using backend-provided links
directly; add a validation step (e.g., create a helper validateUrl or
isSafeProtocol) that uses the URL constructor or regex to allow only safe
protocols (http:, https:, mailto:) and rejects others; update the mapping in the
CalibrationReview component to either filter out unsafe links or render them
with a safe fallback (e.g., plain text or href="#" and no target) when
validateUrl(link) returns false, ensuring the anchor tag's href is never set to
an unsafe scheme.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/pages/Assignments/hooks/useCalibrationInstructorDemo.ts (1)

70-84: Document the stability requirement for onSuccess.

The onSuccess callback is included in the dependency array. If passed an unstable function reference (one not wrapped in useCallback), this effect could fire unexpectedly on every render after a successful response. The current usage in AssignmentEditor.tsx is safe since refreshCalibrationParticipants is memoized, but consider documenting this expectation in the JSDoc or interface.

Suggested documentation update
 interface UseCalibrationInstructorDemoOptions {
   assignmentId: number | string | null | undefined;
-  onSuccess: () => void; // called after a successful seed so the table refreshes
+  /**
+   * Called after a successful seed so the table refreshes.
+   * Must be a stable reference (e.g., wrapped in useCallback) to avoid
+   * re-triggering the effect on every render.
+   */
+  onSuccess: () => void;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/hooks/useCalibrationInstructorDemo.ts` around lines 70
- 84, The effect watching beginCalibrationResponse currently includes onSuccess
in its dependency array; document that onSuccess must be a stable (memoized)
callback to avoid the effect re-running unexpectedly — update the JSDoc or the
hook interface for useCalibrationInstructorDemo to state that onSuccess should
be wrapped with useCallback (or otherwise be stable), and reference the
onSuccess parameter and the useEffect that reads beginCalibrationResponse so
callers (e.g., AssignmentEditor.tsx which passes refreshCalibrationParticipants)
know to memoize their handler.
src/pages/Assignments/AssignmentEditor.tsx (2)

1348-1354: Use React Router's Link instead of <a> for SPA navigation.

Using raw <a href> tags for internal routes causes full page reloads instead of client-side navigation. Since these point to routes within the app (/assignments/edit/.../calibration/...), they should use React Router's Link or useNavigate.

Suggested fix
+import { Link } from "react-router-dom";
// ... in the cell renderer:
-                              return (
-                                <span>
-                                  <a style={linkStyle} href={`${reviewBase}/view`}>View</a>
-                                  <span style={{ margin: '0 0.4rem', color: '#986633' }}>|</span>
-                                  <a style={linkStyle} href={`${reviewBase}/edit`}>Edit</a>
-                                </span>
-                              );
+                              return (
+                                <span>
+                                  <Link style={linkStyle} to={`${reviewBase}/view`}>View</Link>
+                                  <span style={{ margin: '0 0.4rem', color: '#986633' }}>|</span>
+                                  <Link style={linkStyle} to={`${reviewBase}/edit`}>Edit</Link>
+                                </span>
+                              );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/AssignmentEditor.tsx` around lines 1348 - 1354, The
current JSX in AssignmentEditor.tsx renders internal navigation using raw <a>
tags (see the span that returns View/Edit with linkStyle and reviewBase), which
causes full page reloads; replace these <a href={`${reviewBase}/view`}> and <a
href={`${reviewBase}/edit`}> with React Router <Link> components (or
useNavigate) to enable client-side routing, preserving the same linkStyle and
URL paths (e.g., <Link to={`${reviewBase}/view`} style={linkStyle}> and
similarly for edit) and ensure you import Link from react-router-dom at the top
of the file.

1359-1371: Use React Router's Link for the "View review report" link.

Same issue—this internal route should use Link for client-side navigation.

Suggested fix
-                              return (
-                                <a style={{ color: '#986633', textDecoration: 'none' }} href={href}>
-                                  View review report
-                                </a>
-                              );
+                              return (
+                                <Link style={{ color: '#986633', textDecoration: 'none' }} to={href}>
+                                  View review report
+                                </Link>
+                              );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/AssignmentEditor.tsx` around lines 1359 - 1371, The
cell renderer for the "Report" column currently returns an <a> tag using the
computed href (mapId ->
`/assignments/edit/${assignmentData.id}/calibration/${mapId}` or '#'); replace
that anchor with React Router's Link component (import { Link } from
'react-router-dom') and use the Link's "to" prop instead of href, preserving the
inline styles and text ("View review report"); ensure the fallback when mapId is
falsy either renders a disabled/non-clickable element or omits the Link (keep
existing visual style) so client-side navigation is used for valid internal
routes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/Assignments/__tests__/CalibrationReview.test.tsx`:
- Around line 72-74: The test currently waits for screen.getByText(/calibration
report/i) which matches both the heading and the loading message; replace this
with an explicit role-based wait so the test waits for the actual heading.
Update the assertion that uses waitFor + screen.getByText to instead await
screen.findByRole("heading", { name: /calibration report/i }) (or assert its
presence with expect(await screen.findByRole(...)).toBeInTheDocument()) to
ensure you wait specifically for the heading element.

In `@src/pages/Assignments/AssignmentEditor.tsx`:
- Line 1291: In AssignmentEditor.tsx remove the erroneous CSS declaration
display: 'ruby' from the inline style on the div (the <div style={{ display:
'ruby', marginTop: '30px' }}> element) — either delete the display property
entirely or replace it with the intended value (e.g., 'block' or leave only
marginTop) so the calibration table layout is not affected.
- Around line 1372-1399: The cell renderer in AssignmentEditor (the cell: ({ row
}) => ... block rendering row.original.submitted_content?.hyperlinks and files)
currently binds user-provided URLs directly to <a href={item}> which allows
javascript: URIs; update the hyperlink and file href construction to validate
and sanitize the URL before rendering: attempt to parse with the URL constructor
and only allow safe protocols (e.g. http:, https:, mailto:), otherwise treat as
unsafe and either drop the link or render it as plain text; for non-string file
objects use item.path only after the same check; also add safe anchor attributes
like rel="noopener noreferrer" and target="_blank" if opening external links.

---

Nitpick comments:
In `@src/pages/Assignments/AssignmentEditor.tsx`:
- Around line 1348-1354: The current JSX in AssignmentEditor.tsx renders
internal navigation using raw <a> tags (see the span that returns View/Edit with
linkStyle and reviewBase), which causes full page reloads; replace these <a
href={`${reviewBase}/view`}> and <a href={`${reviewBase}/edit`}> with React
Router <Link> components (or useNavigate) to enable client-side routing,
preserving the same linkStyle and URL paths (e.g., <Link
to={`${reviewBase}/view`} style={linkStyle}> and similarly for edit) and ensure
you import Link from react-router-dom at the top of the file.
- Around line 1359-1371: The cell renderer for the "Report" column currently
returns an <a> tag using the computed href (mapId ->
`/assignments/edit/${assignmentData.id}/calibration/${mapId}` or '#'); replace
that anchor with React Router's Link component (import { Link } from
'react-router-dom') and use the Link's "to" prop instead of href, preserving the
inline styles and text ("View review report"); ensure the fallback when mapId is
falsy either renders a disabled/non-clickable element or omits the Link (keep
existing visual style) so client-side navigation is used for valid internal
routes.

In `@src/pages/Assignments/hooks/useCalibrationInstructorDemo.ts`:
- Around line 70-84: The effect watching beginCalibrationResponse currently
includes onSuccess in its dependency array; document that onSuccess must be a
stable (memoized) callback to avoid the effect re-running unexpectedly — update
the JSDoc or the hook interface for useCalibrationInstructorDemo to state that
onSuccess should be wrapped with useCallback (or otherwise be stable), and
reference the onSuccess parameter and the useEffect that reads
beginCalibrationResponse so callers (e.g., AssignmentEditor.tsx which passes
refreshCalibrationParticipants) know to memoize their handler.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a2c13014-0e5c-4b44-96d5-99b679db0de0

📥 Commits

Reviewing files that changed from the base of the PR and between 205b2da and fedab4d.

📒 Files selected for processing (4)
  • src/pages/Assignments/AssignmentEditor.tsx
  • src/pages/Assignments/__tests__/CalibrationReview.test.tsx
  • src/pages/Assignments/__tests__/calibrationReportNormalize.test.ts
  • src/pages/Assignments/hooks/useCalibrationInstructorDemo.ts

Comment on lines +72 to +74
await waitFor(() =>
expect(screen.getByText(/calibration report/i)).toBeInTheDocument()
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify overlapping text that makes the current wait condition ambiguous.
rg -n 'Loading calibration report|getByText\(/calibration report|Calibration Report' \
  src/pages/Assignments/CalibrationReview.tsx \
  src/pages/Assignments/__tests__/CalibrationReview.test.tsx

Repository: expertiza/reimplementation-front-end

Length of output: 418


Wait for the heading element to avoid matching the loading text.

/calibration report/i matches both "Calibration Report" (the actual heading) and "Loading calibration report..." (the loading state), causing the wait to complete too early. Use findByRole("heading") instead, which waits specifically for the heading to appear.

Proposed fix
    await waitFor(() =>
-      expect(screen.getByText(/calibration report/i)).toBeInTheDocument()
+    await screen.findByRole("heading", { name: /calibration report/i });
📝 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.

Suggested change
await waitFor(() =>
expect(screen.getByText(/calibration report/i)).toBeInTheDocument()
);
await screen.findByRole("heading", { name: /calibration report/i });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/__tests__/CalibrationReview.test.tsx` around lines 72 -
74, The test currently waits for screen.getByText(/calibration report/i) which
matches both the heading and the loading message; replace this with an explicit
role-based wait so the test waits for the actual heading. Update the assertion
that uses waitFor + screen.getByText to instead await
screen.findByRole("heading", { name: /calibration report/i }) (or assert its
presence with expect(await screen.findByRole(...)).toBeInTheDocument()) to
ensure you wait specifically for the heading element.

<h3 style={{ marginTop: '1.5rem' }}>Select participants for submitting calibration artifacts</h3>

<div>
<div style={{ display: 'ruby', marginTop: '30px' }}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if display: ruby is used intentionally elsewhere in the codebase
rg "display:\s*['\"]?ruby" --type=ts --type=tsx --type=css --type=scss || echo "No other occurrences found"

Repository: expertiza/reimplementation-front-end

Length of output: 153


display: ruby appears to be a typo.

display: ruby is a CSS value for East Asian Ruby annotation layout and has no other occurrences in the codebase. In the context of a calibration table, this is almost certainly unintended and should be removed.

Suggested fix
-                    <div style={{ display: 'ruby', marginTop: '30px' }}>
+                    <div style={{ marginTop: '30px' }}>
📝 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.

Suggested change
<div style={{ display: 'ruby', marginTop: '30px' }}>
<div style={{ marginTop: '30px' }}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/AssignmentEditor.tsx` at line 1291, In
AssignmentEditor.tsx remove the erroneous CSS declaration display: 'ruby' from
the inline style on the div (the <div style={{ display: 'ruby', marginTop:
'30px' }}> element) — either delete the display property entirely or replace it
with the intended value (e.g., 'block' or leave only marginTop) so the
calibration table layout is not affected.

Comment on lines +1372 to +1399
{
cell: ({ row }) => (
<>
<div>Hyperlinks:</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
{(row.original.submitted_content?.hyperlinks || []).map((item: any, index: number) => (
<a style={{ color: '#986633', textDecoration: 'none' }} key={`hl-${index}`} href={item}>{item}</a>
))}
</div>
<div style={{ marginTop: '10px', display: 'flex', flexDirection: 'column' }}>Files:</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
{(row.original.submitted_content?.files || []).map((item: any, index: number) => {
// Files may arrive as bare strings (report shape) or as
// { id, name, path, submitted_at, submitted_by } objects
// (participant-list shape). Handle both.
const href = typeof item === 'string' ? item : item.path;
const label = typeof item === 'string' ? item : (item.name || item.path);
return (
<a
style={{ color: '#986633', textDecoration: 'none' }}
key={`file-${index}`}
href={href}
>
{label}
</a>
);
})}
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's URL validation/sanitization happening elsewhere for submitted hyperlinks
rg -n "hyperlink|submitted_content" --type=ts -A 5 -B 5 | head -100

Repository: expertiza/reimplementation-front-end

Length of output: 5591


🏁 Script executed:

# 1. Check the exact code at the specified lines in AssignmentEditor.tsx
wc -l src/pages/Assignments/AssignmentEditor.tsx

Repository: expertiza/reimplementation-front-end

Length of output: 128


🏁 Script executed:

# 2. Search for sanitization libraries or URL validation in the project
rg -i "dompurify|sanitize|xss|validator" --type=ts,tsx,json

Repository: expertiza/reimplementation-front-end

Length of output: 116


🏁 Script executed:

# 3. Check if there's any validation on hyperlinks in the submitted_content service
cat -n src/services/SubmittedContentService.ts

Repository: expertiza/reimplementation-front-end

Length of output: 9930


🏁 Script executed:

# Read the specific lines from AssignmentEditor.tsx
sed -n '1372,1399p' src/pages/Assignments/AssignmentEditor.tsx

Repository: expertiza/reimplementation-front-end

Length of output: 2033


🏁 Script executed:

# Fix the syntax and search for sanitization libraries
rg -i "dompurify|sanitize|xss" --type typescript | head -50

Repository: expertiza/reimplementation-front-end

Length of output: 115


🏁 Script executed:

# Check package.json for sanitization dependencies
cat package.json | grep -A 20 -B 5 "dependencies"

Repository: expertiza/reimplementation-front-end

Length of output: 842


🏁 Script executed:

# Search for any URL validation or sanitization on hyperlinks
rg "javascript:|href.*validate|sanitize.*url" --type typescript

Repository: expertiza/reimplementation-front-end

Length of output: 115


🏁 Script executed:

# Show full package.json dependencies
cat package.json

Repository: expertiza/reimplementation-front-end

Length of output: 2246


🏁 Script executed:

# Search for sanitization/validation with correct syntax
rg -i "dompurify|sanitize|xss" --type ts --type tsx | head -50

Repository: expertiza/reimplementation-front-end

Length of output: 108


🏁 Script executed:

# Search for any URL validation patterns or href handling
rg "href|javascript:" --type ts --type tsx -A 2 -B 2 | head -100

Repository: expertiza/reimplementation-front-end

Length of output: 108


🏁 Script executed:

# Search for sanitization with just -t ts (without tsx)
rg -i "dompurify|sanitize.*url|xss" -t ts | head -30

Repository: expertiza/reimplementation-front-end

Length of output: 62


🏁 Script executed:

# Check if there's any backend code in this repo that validates hyperlinks
find . -name "*.rb" -o -name "*.py" 2>/dev/null | head -20

Repository: expertiza/reimplementation-front-end

Length of output: 62


🏁 Script executed:

# Look at how other links are handled in the codebase
rg "href=" -t ts -B 2 -A 2 | head -60

Repository: expertiza/reimplementation-front-end

Length of output: 4223


Sanitize hyperlinks to prevent javascript: URL injection.

User-submitted hyperlinks are rendered directly as <a href={item}>. The backend validation uses new URL() which accepts javascript: URLs. A malicious payload like javascript:alert(1) would execute when clicked.

Suggested defensive check
                                  {(row.original.submitted_content?.hyperlinks || []).map((item: any, index: number) => {
+                                   // Basic sanity check: only render http/https URLs
+                                   const isValidUrl = typeof item === 'string' && /^https?:\/\//i.test(item);
+                                   if (!isValidUrl) return null;
                                    return (
                                      <a style={{ color: '#986633', textDecoration: 'none' }} key={`hl-${index}`} href={item}>{item}</a>
                                    );
                                  })}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/AssignmentEditor.tsx` around lines 1372 - 1399, The
cell renderer in AssignmentEditor (the cell: ({ row }) => ... block rendering
row.original.submitted_content?.hyperlinks and files) currently binds
user-provided URLs directly to <a href={item}> which allows javascript: URIs;
update the hyperlink and file href construction to validate and sanitize the URL
before rendering: attempt to parse with the URL constructor and only allow safe
protocols (e.g. http:, https:, mailto:), otherwise treat as unsafe and either
drop the link or render it as plain text; for non-string file objects use
item.path only after the same check; also add safe anchor attributes like
rel="noopener noreferrer" and target="_blank" if opening external links.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant