Skip to content

E2605: Complete frontend support for specialized rubrics by topic#166

Open
PositivelyBookish wants to merge 13 commits into
expertiza:mainfrom
PositivelyBookish:main
Open

E2605: Complete frontend support for specialized rubrics by topic#166
PositivelyBookish wants to merge 13 commits into
expertiza:mainfrom
PositivelyBookish:main

Conversation

@PositivelyBookish
Copy link
Copy Markdown

@PositivelyBookish PositivelyBookish commented Mar 31, 2026

Summary

This PR completes the frontend support for specialized rubrics by topic in the assignment editor.

It adds the UI and request handling needed for instructors to:

  • enable Review rubric varies by topic?
  • view review-rubric selectors for each topic in the Topics tab
  • assign a different review rubric to each topic
  • preserve the assignment-level review rubric as the default fallback when no topic-specific override is selected

Changes

  • persist vary_by_topic state in the assignment editor
  • load topic-specific rubric mappings from the backend when editing an assignment
  • render per-topic review rubric dropdowns in the Topics tab when topic-specific rubrics are enabled
  • support the combined topic + round case by rendering one rubric selector per topic per round when applicable
  • save topic-specific rubric selections back to the backend through the topic-rubric API flow
  • align assignment editor request/response handling with the backend rubric-by-topic contract
  • include a small questionnaire form fix so locally created questionnaires/rubrics can be submitted correctly during manual testing

Behavior

After this change, the assignment editor supports all expected rubric-selection cases:

  • rubric does not vary by round or topic
  • rubric varies by round only
  • rubric varies by topic only
  • rubric varies by both round and topic

When Review rubric varies by topic? is enabled:

  • each topic gets its own review-rubric selector
  • the selected topic rubric overrides the assignment default
  • topics without an override continue using the default assignment review rubric

Manual verification:

  • enabled topic-specific rubrics in the assignment editor
  • created topics
  • selected different rubrics per topic
  • reloaded the page and confirmed selections persisted

Summary by CodeRabbit

  • New Features

    • Assignments can vary review rubrics by topic (and by round) with per-topic questionnaire selectors
    • Added "Review" questionnaire type and improved questionnaire item-type handling
    • Rubric details now display on the Reviews page when referenced via URL parameters
  • Bug Fixes

    • Rubric weight validation ignores empty inputs; rubric selector disables while a per-topic mapping save is pending; clearer save alerts
  • Tests

    • Added tests for topic-based rubric mappings and questionnaire type mappings
  • Chores

    • Updated .gitignore to exclude dist directory

@PositivelyBookish PositivelyBookish marked this pull request as draft March 31, 2026 02:15
@PositivelyBookish PositivelyBookish marked this pull request as ready for review April 1, 2026 13:05
Comment thread src/utils/axios_client.ts Outdated
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

📝 Walkthrough

Walkthrough

A feature enabling per-topic rubric mappings and rubric variability was added across the Assignments UI and data transformation layers; questionnaire type normalization and question item types were introduced for questionnaires; several tests were added; and minor UI/route/console cleanup plus /dist was added to .gitignore.

Changes

Cohort / File(s) Summary
Assignment editor & mapping logic
src/pages/Assignments/AssignmentEditor.tsx, src/pages/Assignments/AssignmentUtil.ts, src/utils/interfaces.ts
Introduce TopicRubricMapping model and per-topic rubric persistence (POST/PATCH/DELETE to /assignment_questionnaires), track pending saves, persist/restore vary_by_topic, add special author/teammate rubric fields and stable rowKey-based form field naming; update interfaces to include vary_by_topic and assignment_questionnaires extensions; remove debug logs.
Topics UI & handlers
src/pages/Assignments/tabs/TopicsTab.tsx, src/pages/Assignments/tabs/TopicsTab.test.tsx
Add props to support varyByTopic/varyByRound, render per-topic (and per-round) rubric selectors, support disabling while mapping is pending, and wire onTopicRubricChange; tests validate rendering, change callback, and pending-state behavior.
Assignment editor tests
src/pages/Assignments/AssignmentEditor.test.tsx
Add UI tests for special rubric rows (author/teammate) and mapping of form ↔ API transformations including vary_by_topic and assignment_questionnaires mapping.
Questionnaire types & utils
src/pages/Questionnaires/QuestionnaireUtils.tsx, src/pages/Questionnaires/QuestionnaireUtils.test.ts
Add "Review" questionnaire type and QuestionItemTypes; normalize "Review""ReviewQuestionnaire" during request/response transforms; tests added for type mapping.
Questionnaire form & navigation tweaks
src/pages/Questionnaires/QuestionnaireForm.tsx, src/pages/Questionnaires/Questionnaire.tsx
Fetch question item types from "/questions/types" and derive availableItemTypes; change edit route to absolute /questionnaires/edit/:id; remove debug logs.
Reviews page
src/pages/Reviews/reviews.tsx
Read response_map_id/round from URL, fetch /student_tasks/rubric_for, render resolved rubric banner (loading / data / error), and remove debug logs.
Tests (misc)
src/pages/Questionnaires/QuestionnaireUtils.test.ts
New vitest coverage for questionnaire type mapping.
Repo config
.gitignore
Add /dist to ignored paths.

Sequence Diagram(s)

sequenceDiagram
  participant User as "User"
  participant UI as "TopicsTab / AssignmentEditor (Client)"
  participant API as "API (/assignment_questionnaires)"
  participant Store as "Client Mappings Cache"
  participant Alert as "Alerts"

  User->>UI: change topic rubric dropdown
  UI->>UI: isTopicRubricMappingPending? (check pending set)
  UI->>UI: compute action (POST / PATCH / DELETE)
  UI->>API: send request (POST/PATCH/DELETE)
  API-->>UI: response (success / error)
  UI->>Store: refresh mappings (GET /assignment_questionnaires?assignment_id=...)
  Store-->>UI: updated mappings
  UI->>Alert: show success or error message
  UI-->>User: update dropdown state (disabled/aria-busy cleared)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

"I hopped through code with a twitchy nose,
Mapped rubrics by topic where wild logic grows.
POST, PATCH, DELETE — I danced in the stack,
Alerts box a-carrot, and tests guard the track. 🥕"

🚥 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 accurately describes the main change: completing frontend support for specialized rubrics that vary by topic across the assignment editor.
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

Review rate limit: 2/3 reviews remaining, refill in 20 minutes.

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

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/Assignments/AssignmentEditor.tsx (1)

1418-1496: ⚠️ Potential issue | 🟠 Major

Unreachable dead code should be removed.

There are two return statements in this component. The first return at line 772 renders the tab-based editor UI. This second return statement (lines 1418-1496) rendering a Modal-based UI is unreachable and will never execute.

🗑️ Proposed fix: Remove dead code
   );

-  return (
-    <Modal size="lg" centered show={true} onHide={handleClose} backdrop="static">
-      <Modal.Header closeButton>
-        <Modal.Title>{mode === "update" ? `Update Assignment - ${assignmentData.name}` : "Create Assignment"}</Modal.Title>
-      </Modal.Header>
-      <Modal.Body>
-        {assignmentError && <p className="text-danger">{assignmentError}</p>}
-        <Tabs defaultActiveKey="general" id="assignment-tabs">
-          <Tab eventKey="general" title="General">
-            <Formik
-              initialValues={mode === "update" ? assignmentData : initialValues}
-              onSubmit={onSubmit}
-              validationSchema={validationSchema}
-              validateOnChange={false}
-              enableReinitialize={true}
-            >
-              {(formik) => {
-                return (
-                  <Form>
-                    <FormInput controlId="assignment-name" label="Assignment Name" name="name" />
-                    ... (entire Modal block)
-                  </Form>
-                );
-              }}
-            </Formik>
-          </Tab>
-          <Tab eventKey="etc" title="Etc">
-            <EtcTab assignmentId={assignmentData?.id} />
-          </Tab>
-        </Tabs>
-      </Modal.Body>
-    </Modal>
-  );
 };
 
 export default AssignmentEditor;
🤖 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 1418 - 1496, The
second unreachable JSX return in the AssignmentEditor component (the Modal-based
UI starting with <Modal size="lg" centered show={true} onHide={handleClose} ...>
and ending at </Modal>) should be removed; locate the unused/duplicate return
block inside the AssignmentEditor function (the one that renders the Modal and
Et cTab) and delete that entire block so only the primary tab-based return
remains, and then run the app/build to ensure no missing references (remove any
stray variables or imports that were only used by the deleted Modal if the
linter flags them).
🧹 Nitpick comments (4)
src/pages/Questionnaires/QuestionnaireUtils.tsx (1)

112-114: Use a shared bidirectional questionnaire-type map.

Request transformation normalizes types, but response transformation currently does not. Centralizing both directions avoids backend/UI enum drift.

Refactor sketch
+const BACKEND_TYPE_BY_FORM_TYPE: Record<string, string> = {
+  Review: "ReviewQuestionnaire",
+  "Author feedback": "AuthorFeedback",
+  "Teammate Review": "TeammateReview",
+  Survey: "Survey",
+  "Assignment survey": "AssignmentSurvey",
+  "Global survey": "GlobalSurvey",
+  "Course survey": "CourseSurvey",
+  "Bookmark rating": "BookmarkRating",
+  Quiz: "Quiz",
+};
+
+const FORM_TYPE_BY_BACKEND_TYPE = Object.fromEntries(
+  Object.entries(BACKEND_TYPE_BY_FORM_TYPE).map(([k, v]) => [v, k])
+);
+
 export const transformQuestionnaireRequest = (values: QuestionnaireFormValues) => {
   const questionnaire: QuestionnaireRequest = {
@@
-    questionnaire_type: values.questionnaire_type === "Review"
-      ? "ReviewQuestionnaire"
-      : values.questionnaire_type.replace(/\s+/g, ""),
+    questionnaire_type:
+      BACKEND_TYPE_BY_FORM_TYPE[values.questionnaire_type] ??
+      values.questionnaire_type.replace(/\s+/g, ""),
@@
 export const transformQuestionnaireResponse = (data: any): QuestionnaireFormValues => {
   return {
@@
-    questionnaire_type: data.questionnaire_type,
+    questionnaire_type:
+      FORM_TYPE_BY_BACKEND_TYPE[data.questionnaire_type] ?? data.questionnaire_type,

Also applies to: 131-137

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

In `@src/pages/Questionnaires/QuestionnaireUtils.tsx` around lines 112 - 114, The
code normalizes questionnaire_type on request using a custom replace/ternary but
lacks a shared bidirectional map for request/response leading to enum drift;
create a single mapping object (e.g., QUESTIONNAIRE_TYPE_MAP with keys as UI
labels and values as backend enums and derive the inverse map) and replace the
inline logic in the request transform (where questionnaire_type is set from
values.questionnaire_type) and the response transform (the block around lines
131-137) to use this shared map and its inverse so both directions consistently
translate types.
src/pages/Assignments/AssignmentEditor.test.tsx (1)

227-250: Consider adding test coverage for the default/fallback case.

The test verifies that vary_by_topic: true is correctly mapped, but there's no assertion that review_rubric_varies_by_topic defaults to false when vary_by_topic is absent from the backend response. This default behavior is mentioned in the summary and implemented in transformAssignmentResponse.

💡 Suggested additional test case
   expect(values.review_rubric_varies_by_topic).toBe(true);
  });
+
+  it("defaults review_rubric_varies_by_topic to false when vary_by_topic is absent", () => {
+    const assignment = {
+      id: 1,
+      name: "Test Assignment",
+      directory_path: "assignment_1",
+      spec_location: "http://example.com",
+      private: false,
+      show_template_review: false,
+      require_quiz: false,
+      has_badge: false,
+      staggered_deadline: false,
+      is_calibrated: false,
+      num_review_rounds: 1,
+      due_dates: [],
+      assignment_questionnaires: [],
+    };
+
+    const values = transformAssignmentResponse(JSON.stringify(assignment));
+
+    expect(values.review_rubric_varies_by_topic).toBe(false);
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Assignments/AssignmentEditor.test.tsx` around lines 227 - 250, Add
a test to cover the default/fallback behavior when vary_by_topic is missing:
call transformAssignmentResponse with an assignment object that does NOT include
the vary_by_topic property (e.g., omit that key from the payload or delete it)
and assert that the returned values.review_rubric_varies_by_topic is false;
reference the existing test block for transformAssignmentResponse and mirror its
structure (use JSON.stringify on the assignment payload and the same expect
style) to ensure the default mapping is verified.
src/pages/Assignments/tabs/TopicsTab.tsx (2)

343-350: Duplicate findTopicRubricMapping implementation.

This function has an identical implementation in AssignmentEditor.tsx (lines 535-540). While TopicsTab needs it for rendering and AssignmentEditor needs it for the change handler, consider whether one component could pass the resolved mapping value directly to avoid the duplication.

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

In `@src/pages/Assignments/tabs/TopicsTab.tsx` around lines 343 - 350, The
findTopicRubricMapping implementation is duplicated between TopicsTab and
AssignmentEditor; remove the duplication by moving the logic up to a common
owner or by having the component that already computes it pass the resolved
value down. Specifically, replace the local findTopicRubricMapping in TopicsTab
with a prop (e.g., topicRubricMapping or a getTopicRubricMapping function)
provided by the parent or by AssignmentEditor, using the same matching logic
against topicRubricMappings (compare Number(mapping.project_topic_id) to
Number(topicDatabaseId) and compare used_in_round with usedInRound via
null-coalescing). Update TopicsTab usage sites to consume the passed mapping or
call the single shared getTopicRubricMapping so only one implementation remains.

59-70: Consider extracting shared types to avoid duplication.

The TopicRubricMapping interface is duplicated in both TopicsTab.tsx and AssignmentEditor.tsx. Consider extracting it to a shared location (e.g., interfaces.ts or a dedicated types file) to maintain a single source of truth.

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

In `@src/pages/Assignments/tabs/TopicsTab.tsx` around lines 59 - 70, The
TopicRubricMapping interface is duplicated; extract it (and RubricOption if also
reused) into a shared types file (e.g., create a new interfaces/types module),
export TopicRubricMapping from that module, update TopicsTab.tsx and
AssignmentEditor.tsx to import TopicRubricMapping (and RubricOption) instead of
declaring them locally, remove the duplicate declarations, and run TypeScript
checks to ensure no unresolved symbols remain.
🤖 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/Questionnaires/QuestionnaireForm.tsx`:
- Around line 13-15: availableItemTypes currently maps itemTypes.data twice and
duplicates work; change the logic in the availableItemTypes constant to compute
the mapped array once (e.g., assign const mapped = itemTypes?.data?.map((t: any)
=> t.name) as string[] and then use mapped.length ? mapped : QuestionItemTypes)
so the mapping is not performed twice, and remove the debug console.log that
prints fetched data during render (the debug logging referenced near line 21) to
avoid logging in the render lifecycle.

In `@src/pages/Questionnaires/QuestionnaireUtils.tsx`:
- Line 108: Remove the two noisy debug console.log statements that print full
form/request objects (specifically the console.log("Original Form Values:",
values) call and the similar log at line 127) so sensitive payloads are not
exposed; either delete these console.log calls or replace them with a sanitized
or conditional debug log (use a dedicated logger or an isDebug flag and redact
PII) and update any tests or usages relying on those logs; search for the exact
string "Original Form Values:" and the corresponding request object log in
QuestionnaireUtils.tsx to locate and remove/replace them.

In `@src/pages/Reviews/reviews.tsx`:
- Around line 88-105: The effect calling fetchResolvedRubric should clear stale
state and handle the returned promise to avoid unhandled rejections: when
responseMapId or reviewRound changes, immediately setResolvedRubric(null) (or
undefined) to clear the previous rubric, then invoke fetchResolvedRubric inside
an async IIFE or promise chain and wrap it in try/catch so any thrown error from
fetchResolvedRubric is caught and handled (e.g., log it or set an error state)
instead of bubbling as an unhandled rejection; keep the second effect that syncs
rubricResponse.data to setResolvedRubric but ensure you don’t overwrite the
cleared state on error.

---

Outside diff comments:
In `@src/pages/Assignments/AssignmentEditor.tsx`:
- Around line 1418-1496: The second unreachable JSX return in the
AssignmentEditor component (the Modal-based UI starting with <Modal size="lg"
centered show={true} onHide={handleClose} ...> and ending at </Modal>) should be
removed; locate the unused/duplicate return block inside the AssignmentEditor
function (the one that renders the Modal and Et cTab) and delete that entire
block so only the primary tab-based return remains, and then run the app/build
to ensure no missing references (remove any stray variables or imports that were
only used by the deleted Modal if the linter flags them).

---

Nitpick comments:
In `@src/pages/Assignments/AssignmentEditor.test.tsx`:
- Around line 227-250: Add a test to cover the default/fallback behavior when
vary_by_topic is missing: call transformAssignmentResponse with an assignment
object that does NOT include the vary_by_topic property (e.g., omit that key
from the payload or delete it) and assert that the returned
values.review_rubric_varies_by_topic is false; reference the existing test block
for transformAssignmentResponse and mirror its structure (use JSON.stringify on
the assignment payload and the same expect style) to ensure the default mapping
is verified.

In `@src/pages/Assignments/tabs/TopicsTab.tsx`:
- Around line 343-350: The findTopicRubricMapping implementation is duplicated
between TopicsTab and AssignmentEditor; remove the duplication by moving the
logic up to a common owner or by having the component that already computes it
pass the resolved value down. Specifically, replace the local
findTopicRubricMapping in TopicsTab with a prop (e.g., topicRubricMapping or a
getTopicRubricMapping function) provided by the parent or by AssignmentEditor,
using the same matching logic against topicRubricMappings (compare
Number(mapping.project_topic_id) to Number(topicDatabaseId) and compare
used_in_round with usedInRound via null-coalescing). Update TopicsTab usage
sites to consume the passed mapping or call the single shared
getTopicRubricMapping so only one implementation remains.
- Around line 59-70: The TopicRubricMapping interface is duplicated; extract it
(and RubricOption if also reused) into a shared types file (e.g., create a new
interfaces/types module), export TopicRubricMapping from that module, update
TopicsTab.tsx and AssignmentEditor.tsx to import TopicRubricMapping (and
RubricOption) instead of declaring them locally, remove the duplicate
declarations, and run TypeScript checks to ensure no unresolved symbols remain.

In `@src/pages/Questionnaires/QuestionnaireUtils.tsx`:
- Around line 112-114: The code normalizes questionnaire_type on request using a
custom replace/ternary but lacks a shared bidirectional map for request/response
leading to enum drift; create a single mapping object (e.g.,
QUESTIONNAIRE_TYPE_MAP with keys as UI labels and values as backend enums and
derive the inverse map) and replace the inline logic in the request transform
(where questionnaire_type is set from values.questionnaire_type) and the
response transform (the block around lines 131-137) to use this shared map and
its inverse so both directions consistently translate types.
🪄 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: 6d4ced0f-4a68-4967-8168-e187c4c0f5eb

📥 Commits

Reviewing files that changed from the base of the PR and between 474e9f7 and 7b13d83.

📒 Files selected for processing (12)
  • .gitignore
  • src/pages/Assignments/AssignmentEditor.test.tsx
  • src/pages/Assignments/AssignmentEditor.tsx
  • src/pages/Assignments/AssignmentUtil.ts
  • src/pages/Assignments/tabs/TopicsTab.test.tsx
  • src/pages/Assignments/tabs/TopicsTab.tsx
  • src/pages/Questionnaires/Questionnaire.tsx
  • src/pages/Questionnaires/QuestionnaireForm.tsx
  • src/pages/Questionnaires/QuestionnaireUtils.test.ts
  • src/pages/Questionnaires/QuestionnaireUtils.tsx
  • src/pages/Reviews/reviews.tsx
  • src/utils/interfaces.ts

Comment on lines +13 to +15
const availableItemTypes = (itemTypes?.data?.map((t: any) => t.name) as string[])?.length
? (itemTypes.data.map((t: any) => t.name) as string[])
: QuestionItemTypes;
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

Deduplicate item-type mapping and remove debug logging.

Line 13–15 maps the same array twice; Line 21 logs fetched data during render lifecycle.

Suggested fix
-    const availableItemTypes = (itemTypes?.data?.map((t: any) => t.name) as string[])?.length
-      ? (itemTypes.data.map((t: any) => t.name) as string[])
-      : QuestionItemTypes;
+    const mappedItemTypes = Array.isArray(itemTypes?.data)
+      ? (itemTypes.data.map((t: any) => t.name) as string[])
+      : [];
+    const availableItemTypes = mappedItemTypes.length ? mappedItemTypes : QuestionItemTypes;
@@
-            fetchItemTypes({ url: "/questions/types" });
-          console.log(itemTypes?.data);
+            fetchItemTypes({ url: "/questions/types" });
         }, [fetchItemTypes]);

Also applies to: 21-21

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

In `@src/pages/Questionnaires/QuestionnaireForm.tsx` around lines 13 - 15,
availableItemTypes currently maps itemTypes.data twice and duplicates work;
change the logic in the availableItemTypes constant to compute the mapped array
once (e.g., assign const mapped = itemTypes?.data?.map((t: any) => t.name) as
string[] and then use mapped.length ? mapped : QuestionItemTypes) so the mapping
is not performed twice, and remove the debug console.log that prints fetched
data during render (the debug logging referenced near line 21) to avoid logging
in the render lifecycle.

Comment thread src/pages/Questionnaires/QuestionnaireUtils.tsx Outdated
Comment on lines +88 to +105
useEffect(() => {
if (!responseMapId) return;

fetchResolvedRubric({
url: "/student_tasks/rubric_for",
method: "GET",
params: {
response_map_id: responseMapId,
...(reviewRound ? { round: reviewRound } : {}),
},
});
}, [fetchResolvedRubric, responseMapId, reviewRound]);

useEffect(() => {
if (rubricResponse?.data) {
setResolvedRubric(rubricResponse.data);
}
}, [rubricResponse]);
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

Handle the fetch promise and clear stale rubric state.

fetchResolvedRubric rethrows on failure via useAPI, but this effect neither awaits nor catches the promise, so a failed lookup will surface as an unhandled rejection. It also leaves resolvedRubric populated until the next response arrives, which can briefly show the previous rubric after response_map_id or round changes.

♻️ Proposed fix
   useEffect(() => {
+    setResolvedRubric(null);
     if (!responseMapId) return;

-    fetchResolvedRubric({
+    void fetchResolvedRubric({
       url: "/student_tasks/rubric_for",
       method: "GET",
       params: {
         response_map_id: responseMapId,
         ...(reviewRound ? { round: reviewRound } : {}),
       },
-    });
+    }).catch(() => {});
   }, [fetchResolvedRubric, responseMapId, reviewRound]);
📝 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
useEffect(() => {
if (!responseMapId) return;
fetchResolvedRubric({
url: "/student_tasks/rubric_for",
method: "GET",
params: {
response_map_id: responseMapId,
...(reviewRound ? { round: reviewRound } : {}),
},
});
}, [fetchResolvedRubric, responseMapId, reviewRound]);
useEffect(() => {
if (rubricResponse?.data) {
setResolvedRubric(rubricResponse.data);
}
}, [rubricResponse]);
useEffect(() => {
setResolvedRubric(null);
if (!responseMapId) return;
void fetchResolvedRubric({
url: "/student_tasks/rubric_for",
method: "GET",
params: {
response_map_id: responseMapId,
...(reviewRound ? { round: reviewRound } : {}),
},
}).catch(() => {});
}, [fetchResolvedRubric, responseMapId, reviewRound]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Reviews/reviews.tsx` around lines 88 - 105, The effect calling
fetchResolvedRubric should clear stale state and handle the returned promise to
avoid unhandled rejections: when responseMapId or reviewRound changes,
immediately setResolvedRubric(null) (or undefined) to clear the previous rubric,
then invoke fetchResolvedRubric inside an async IIFE or promise chain and wrap
it in try/catch so any thrown error from fetchResolvedRubric is caught and
handled (e.g., log it or set an error state) instead of bubbling as an unhandled
rejection; keep the second effect that syncs rubricResponse.data to
setResolvedRubric but ensure you don’t overwrite the cleared state on error.

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

♻️ Duplicate comments (1)
src/pages/Reviews/reviews.tsx (1)

87-104: ⚠️ Potential issue | 🟠 Major

Handle async failures and clear stale rubric before each lookup.

At Line 87-Line 98, fetchResolvedRubric(...) is fired without handling the returned promise, and previous resolvedRubric is not cleared before a new request. Since useAPI.sendRequest rethrows, this can produce unhandled rejections and stale rubric display until the next response.

♻️ Suggested fix
   useEffect(() => {
+    setResolvedRubric(null);
     if (!responseMapId) return;
 
-    fetchResolvedRubric({
+    void fetchResolvedRubric({
       url: "/student_tasks/rubric_for",
       method: "GET",
       params: {
         response_map_id: responseMapId,
         ...(reviewRound ? { round: reviewRound } : {}),
       },
-    });
+    }).catch(() => {
+      // keep rubric cleared; error is already exposed through rubricError
+    });
   }, [fetchResolvedRubric, responseMapId, reviewRound]);
#!/bin/bash
# Verify the risk:
# 1) useAPI rethrows request failures
# 2) this effect does not catch/void-handle the promise
rg -nP 'throw err' src/hooks/useAPI.ts
rg -nP 'useEffect\(\(\)\s*=>\s*\{[\s\S]{0,400}fetchResolvedRubric\(' src/pages/Reviews/reviews.tsx
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Reviews/reviews.tsx` around lines 87 - 104, Before firing
fetchResolvedRubric, clear the previous resolvedRubric and avoid unhandled
rejections by awaiting the request inside an async function and catching errors;
specifically, in the effect that currently calls fetchResolvedRubric, call
setResolvedRubric(null) (or equivalent) then run an async IIFE that awaits
fetchResolvedRubric({...}) and wraps it in try/catch to handle/log the error (or
swallow it), and keep the existing separate effect that sets resolvedRubric from
rubricResponse as-is.
🤖 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 954-974: The rubric fields must share one stable row key instead
of using row.original.id in some places; create a single derived rowKey (e.g.,
if row.original.title === "Author feedback:" use "author_feedback", else if
"Teammate review:" use "teammate_review", otherwise use `round_${row.original.id
|| 0}`) and replace all usages of questionnaireFieldName/questionnaireControlId
and the sibling weight and notification-limit name/controlId generation to use
that rowKey (update FormSelect's name/controlId and the corresponding weight and
notification-limit Formik paths) so the questionnaire, weight, and
notification-limit fields use the same stable identifier across the component.
- Around line 538-585: handleTopicRubricChange reads existingMapping before the
async save which can be stale and cause duplicate POSTs or missed DELETEs; fix
by making the mutation update topicRubricMappings optimistically or by locking
the selector until the change completes. Specifically, when calling
saveTopicRubricMapping in handleTopicRubricChange, immediately update the
in-memory topicRubricMappings (or set a pending map keyed by topicDatabaseId) to
reflect the expected result from the POST/PATCH/DELETE so subsequent rapid
invocations use the updated state instead of findTopicRubricMapping’s stale
value, and clear the pending/optimistic update when refreshTopicRubricMappings
resolves or when the API returns an error.

In `@src/pages/Questionnaires/QuestionnaireUtils.tsx`:
- Around line 129-135: transformQuestionnaireResponse currently returns
questionnaire_type unchanged; add inbound normalization to reverse the outbound
mapping (so UI-facing values match form options). In
transformQuestionnaireResponse map backend value "ReviewQuestionnaire" back to
"Review" (and preserve other values unchanged) for the questionnaire_type field
so edit forms receive the same option keys that the UI expects.

---

Duplicate comments:
In `@src/pages/Reviews/reviews.tsx`:
- Around line 87-104: Before firing fetchResolvedRubric, clear the previous
resolvedRubric and avoid unhandled rejections by awaiting the request inside an
async function and catching errors; specifically, in the effect that currently
calls fetchResolvedRubric, call setResolvedRubric(null) (or equivalent) then run
an async IIFE that awaits fetchResolvedRubric({...}) and wraps it in try/catch
to handle/log the error (or swallow it), and keep the existing separate effect
that sets resolvedRubric from rubricResponse as-is.
🪄 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: efcaef48-4634-4aa2-bd6c-6049a1d923d6

📥 Commits

Reviewing files that changed from the base of the PR and between 7b13d83 and 587533b.

📒 Files selected for processing (6)
  • src/pages/Assignments/AssignmentEditor.tsx
  • src/pages/Assignments/tabs/TopicsTab.tsx
  • src/pages/Questionnaires/Questionnaire.tsx
  • src/pages/Questionnaires/QuestionnaireForm.tsx
  • src/pages/Questionnaires/QuestionnaireUtils.tsx
  • src/pages/Reviews/reviews.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/pages/Questionnaires/Questionnaire.tsx
  • src/pages/Questionnaires/QuestionnaireForm.tsx

Comment thread src/pages/Assignments/AssignmentEditor.tsx Outdated
Comment thread src/pages/Assignments/AssignmentEditor.tsx
Comment thread src/pages/Questionnaires/QuestionnaireUtils.tsx
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: 1

🤖 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 993-1002: The two FormSelect controls in AssignmentEditor that set
questionnaireFieldName to "author_feedback_questionnaire" and
"teammate_review_questionnaire" are using keys that transformAssignmentRequest
does not serialize (it expects questionnaire_round_${i} and writes into
assignment_questionnaires_attributes); update either the control names or the
serializer: change the names set in AssignmentEditor (where FormSelect uses
questionnaireFieldName) to follow the questionnaire_round_${i} naming convention
used by transformAssignmentRequest, or modify transformAssignmentRequest (and
any response mapping) to accept and emit the new keys
("author_feedback_questionnaire" and "teammate_review_questionnaire") and ensure
they are merged into assignment_questionnaires_attributes so selections persist
on save/reload.
🪄 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: 4622883c-91f2-4f09-b5a9-0863d78f8b9e

📥 Commits

Reviewing files that changed from the base of the PR and between 587533b and b4f32d0.

📒 Files selected for processing (6)
  • src/pages/Assignments/AssignmentEditor.test.tsx
  • src/pages/Assignments/AssignmentEditor.tsx
  • src/pages/Assignments/tabs/TopicsTab.test.tsx
  • src/pages/Assignments/tabs/TopicsTab.tsx
  • src/pages/Questionnaires/QuestionnaireUtils.test.ts
  • src/pages/Questionnaires/QuestionnaireUtils.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/pages/Questionnaires/QuestionnaireUtils.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/pages/Assignments/tabs/TopicsTab.test.tsx
  • src/pages/Assignments/AssignmentEditor.test.tsx

Comment thread src/pages/Assignments/AssignmentEditor.tsx
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: 2

🧹 Nitpick comments (1)
src/pages/Assignments/AssignmentEditor.test.tsx (1)

106-138: ⚡ Quick win

Exercise the special-rubric selects through the rendered UI, not just their field names.

This test only checks DOM ids/names. It would still pass if the comboboxes emitted label strings while the submit path expects numeric questionnaire_id values. A render/select/submit assertion for the Author feedback and Teammate review rows would cover the real regression surface here.

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

In `@src/pages/Assignments/AssignmentEditor.test.tsx` around lines 106 - 138,
Update the test to actually interact with the rendered selects and submit the
form so we assert numeric questionnaire_id values are sent: use
render(<AssignmentEditor ...>), locate the Author feedback and Teammate review
comboboxes via getRow(...) and within(...).getByRole("combobox"), change their
selected value to a real numeric questionnaire id (e.g. by firing a change event
with a numeric value), submit the editor form (trigger the submit handler or
click the save button), and assert the submit payload contains questionnaire_id
fields with the expected numeric values for AUTHOR_FEEDBACK_RUBRIC_ROW_KEY and
TEAMMATE_REVIEW_RUBRIC_ROW_KEY rather than string labels; keep the existing
id/name assertions (expectRubricFields) and add the render/select/submit
assertions using the same helpers and constants (AssignmentEditor, getRow,
AUTHOR_FEEDBACK_QUESTIONNAIRE_FIELD, TEAMMATE_REVIEW_QUESTIONNAIRE_FIELD,
AUTHOR_FEEDBACK_RUBRIC_ROW_KEY, TEAMMATE_REVIEW_RUBRIC_ROW_KEY).
🤖 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 776-783: The prefill uses numeric questionnaire ids via
getSpecialRubricQuestionnaireField and formInitialValues but the Author feedback
/ Teammate review dropdowns supply string label values, breaking update-mode and
causing transformAssignmentRequest to send a string questionnaire_id; fix by
changing those dropdown options to objects { label: q.name, value: q.id } (same
pattern used by other rubric selectors) and ensure the select components for the
Author feedback and Teammate review fields read/write the value as the numeric
id (so formInitialValues[specialRubricField.questionnaireField] matches
option.value) and update transformAssignmentRequest to expect/send the numeric
id from those selects.
- Around line 756-761: The review-rubric rows in the Rubrics table still use the
generic questionnaireOptions, allowing non-review questionnaires to be saved;
change those rows to use the filtered reviewRubricOptions instead. Locate where
the Rubrics table renders the review-round/default rows (the code that currently
references questionnaireOptions) and replace the data source/prop with
reviewRubricOptions so only items from the review-questionnaire filter are
selectable/saved; also ensure any prop passed down to TopicsTab or its child
components that represent review-rubric choices uses reviewRubricOptions (not
questionnaireOptions) to keep behavior consistent.

---

Nitpick comments:
In `@src/pages/Assignments/AssignmentEditor.test.tsx`:
- Around line 106-138: Update the test to actually interact with the rendered
selects and submit the form so we assert numeric questionnaire_id values are
sent: use render(<AssignmentEditor ...>), locate the Author feedback and
Teammate review comboboxes via getRow(...) and
within(...).getByRole("combobox"), change their selected value to a real numeric
questionnaire id (e.g. by firing a change event with a numeric value), submit
the editor form (trigger the submit handler or click the save button), and
assert the submit payload contains questionnaire_id fields with the expected
numeric values for AUTHOR_FEEDBACK_RUBRIC_ROW_KEY and
TEAMMATE_REVIEW_RUBRIC_ROW_KEY rather than string labels; keep the existing
id/name assertions (expectRubricFields) and add the render/select/submit
assertions using the same helpers and constants (AssignmentEditor, getRow,
AUTHOR_FEEDBACK_QUESTIONNAIRE_FIELD, TEAMMATE_REVIEW_QUESTIONNAIRE_FIELD,
AUTHOR_FEEDBACK_RUBRIC_ROW_KEY, TEAMMATE_REVIEW_RUBRIC_ROW_KEY).
🪄 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: bf838922-97a5-4c8a-9348-bdf256c1f760

📥 Commits

Reviewing files that changed from the base of the PR and between b4f32d0 and 953b281.

📒 Files selected for processing (3)
  • src/pages/Assignments/AssignmentEditor.test.tsx
  • src/pages/Assignments/AssignmentEditor.tsx
  • src/pages/Assignments/AssignmentUtil.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pages/Assignments/AssignmentUtil.ts

Comment on lines +756 to +761
const reviewRubricOptions = (assignmentData.questionnaires || [])
.filter((q: any) => q.questionnaire_type === "ReviewQuestionnaire")
.map((q: any) => ({
label: q.name,
value: q.id,
}));
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 | ⚡ Quick win

Use reviewRubricOptions for the assignment review-rubric rows too.

This filtered list only gets passed into TopicsTab; the review-round/default rows in the Rubrics table still read from questionnaireOptions, so non-review questionnaires can still be saved as the assignment’s review rubric once they appear in assignmentData.questionnaires.

Suggested patch
{
  id: i + 1,
  rowKey: i + 1,
  title: `Review round ${i + 1}:`,
- questionnaire_options: questionnaireOptions,
+ questionnaire_options: reviewRubricOptions,
  selected_questionnaire: roundSelections[i + 1]?.id,
  questionnaire_type: 'dropdown',
},
...
{
  id: 0,
  rowKey: 0,
  title: "Review rubric:",
- questionnaire_options: questionnaireOptions,
+ questionnaire_options: reviewRubricOptions,
  selected_questionnaire: roundSelections[1]?.id,
  questionnaire_type: 'dropdown',
},
🤖 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 756 - 761, The
review-rubric rows in the Rubrics table still use the generic
questionnaireOptions, allowing non-review questionnaires to be saved; change
those rows to use the filtered reviewRubricOptions instead. Locate where the
Rubrics table renders the review-round/default rows (the code that currently
references questionnaireOptions) and replace the data source/prop with
reviewRubricOptions so only items from the review-questionnaire filter are
selectable/saved; also ensure any prop passed down to TopicsTab or its child
components that represent review-rubric choices uses reviewRubricOptions (not
questionnaireOptions) to keep behavior consistent.

Comment on lines +776 to +783
const specialRubricField = getSpecialRubricQuestionnaireField(
aq.used_in_round,
aq.questionnaire?.questionnaire_type ?? aq.questionnaire_type
);

if (specialRubricField) {
formInitialValues[specialRubricField.questionnaireField] = questionnaireId;
formInitialValues[specialRubricField.assignmentQuestionnaireIdField] = aq.id;
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 | ⚡ Quick win

Keep the special-rubric field values aligned with the select option values.

These fields are now prefilled with numeric questionnaire ids, but the Author feedback / Teammate review dropdowns still use label strings as their option value. That breaks update-mode prefill and causes transformAssignmentRequest to send a string questionnaire_id after a user change instead of the backend questionnaire id.

Please back those dropdowns with { label: q.name, value: q.id } options from the API, the same way the other rubric selectors are handled.

🤖 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 776 - 783, The
prefill uses numeric questionnaire ids via getSpecialRubricQuestionnaireField
and formInitialValues but the Author feedback / Teammate review dropdowns supply
string label values, breaking update-mode and causing transformAssignmentRequest
to send a string questionnaire_id; fix by changing those dropdown options to
objects { label: q.name, value: q.id } (same pattern used by other rubric
selectors) and ensure the select components for the Author feedback and Teammate
review fields read/write the value as the numeric id (so
formInitialValues[specialRubricField.questionnaireField] matches option.value)
and update transformAssignmentRequest to expect/send the numeric id from those
selects.

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.

3 participants