Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import ProtectedRoute from "./router/ProtectedRoute";
import { ROLE } from "./utils/interfaces";
import AssignReviewer from "./pages/Assignments/AssignReviewer";
import StudentTasks from "./pages/StudentTasks/StudentTasks";
import AssignedReviews from "./pages/StudentTasks/AssignedReviews";
import StudentTeams from "./pages/Student Teams/StudentTeamView";
import StudentTeamView from "./pages/Student Teams/StudentTeamView";
import NewTeammateAdvertisement from './pages/Student Teams/NewTeammateAdvertisement';
Expand Down Expand Up @@ -74,6 +75,7 @@ function App() {
{
path: "edit-questionnaire",
element: <ProtectedRoute element={<Questionnaire />} />,
loader: loadQuestionnaire,
},

{
Expand Down Expand Up @@ -295,6 +297,10 @@ function App() {
path: "email_the_author",
element: <Email_the_author />,
},
{
path: "student_tasks/reviews",
element: <ProtectedRoute element={<AssignedReviews />} />,
},
{
path: "duties",
element: <ProtectedRoute element={<Duties />} leastPrivilegeRole={ROLE.TA} />,
Expand All @@ -311,6 +317,10 @@ function App() {
path: "student_tasks/:assignmentId",
element: <ProtectedRoute element={<StudentTasks />} />,
},
{
path: "student_tasks/:assignmentId/reviews",
element: <ProtectedRoute element={<AssignedReviews />} />,
},
{
path: "assignments/:id/review",
element: <ReviewReportPage />,
Expand Down
1 change: 1 addition & 0 deletions src/components/Form/FormSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const FormSelect: React.FC<IFormPropsWithOption & { onChange?: (event: React.Cha
{inputGroupPrepend}
<Form.Select
{...field}
value={field.value ?? ""}
type={type}
disabled={disabled}
isInvalid={isInvalid}
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useSignupSheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const useSignupSheet = (assignmentId: string) => {

// Fetch sign up topics for the assignment
const topicsResponse = await axios.get<SignUpTopic[]>(
`${API_BASE_URL}/sign_up_topics`,
`${API_BASE_URL}/project_topics`,
{
params: { assignment_id: assignmentId },
headers,
Expand Down
64 changes: 58 additions & 6 deletions src/pages/Assignments/AssignReviewer.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// src/pages/Assignments/AssignReviewer.tsx
import React, { useMemo, useState } from "react";
import { Container, Row, Col, Form, Button } from "react-bootstrap";
import { useLocation, useParams } from "react-router-dom";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import axiosClient from "../../utils/axios_client";

type Id = number;
type ReviewStatus = "Not saved" | "Saved" | "Submitted";

interface Assignment { id: Id; name: string }
interface Team { id: Id; name: string; parent_id: Id; mentor_id?: Id | null }
interface Team { id: Id; name: string; parent_id: Id; mentor_id?: Id | null; quiz_questionnaire_id?: Id | null }
interface User { id: Id; name: string | null; full_name: string | null }
interface TeamUser { team_id: Id; user_id: Id }
interface Participant { id: Id; user_id: Id; parent_id: Id; team_id?: Id | null }
Expand All @@ -21,7 +22,7 @@ interface ResponseRow {

interface IUserView { id: Id; username: string; fullName: string }
interface IReviewerAssignment { id: Id; reviewer: IUserView; status: ReviewStatus }
interface ITeamRow { id: Id; name: string; mentor?: IUserView; members: IUserView[]; reviewers: IReviewerAssignment[] }
interface ITeamRow { id: Id; name: string; mentor?: IUserView; members: IUserView[]; reviewers: IReviewerAssignment[]; quiz_questionnaire_id?: Id | null }

type Persist = {
assignment: Assignment;
Expand Down Expand Up @@ -292,6 +293,7 @@ export function demo(asgId: Id): Persist {
const AssignReviewer: React.FC = () => {
const location = useLocation();
const params = useParams();
const navigate = useNavigate();
const maybeId = parseAssignmentId(location, params);

// Hooks must be unconditionally called:
Expand Down Expand Up @@ -383,7 +385,7 @@ const AssignReviewer: React.FC = () => {
})
.filter(Boolean) as IReviewerAssignment[];

return { id: teamId, name: t?.name ?? `Team #${teamId}`, mentor, members, reviewers };
return { id: teamId, name: t?.name ?? `Team #${teamId}`, mentor, members, reviewers, quiz_questionnaire_id: t?.quiz_questionnaire_id ?? null };
});
}, [assignmentId, teams, usersById, teamsById, teamMembersByTeam, response_maps, latestResponseByMap, participantsById, tick]);

Expand All @@ -395,13 +397,16 @@ const AssignReviewer: React.FC = () => {
setTimeout(() => setTick(v => v + 1), 0);
}

function onAddReviewer(teamId: number) {
async function onAddReviewer(teamId: number) {
if (!hasValidId) return;
const raw = window.prompt("Enter reviewer user_id to add for this team:");
if (!raw) return;
const reviewerUserId = Number(raw);
if (!Number.isFinite(reviewerUserId)) { window.alert("Invalid user_id."); return; }

// Track local map id so we can patch it after the backend responds
let localMapId: number | null = null;

mutate(p => {
let reviewerPart = p.participants.find(x => x.user_id === reviewerUserId && x.parent_id === assignmentId);
if (!reviewerPart) {
Expand All @@ -412,15 +417,40 @@ const AssignReviewer: React.FC = () => {
p.users.push({ id: reviewerUserId, name: `user_${reviewerUserId}`, full_name: `user_${reviewerUserId}` });
}
}
localMapId = p.nextMapId++;
p.response_maps.push({
id: p.nextMapId++,
id: localMapId,
reviewed_object_id: assignmentId,
reviewer_id: reviewerPart.id,
reviewer_user_id: reviewerUserId,
reviewee_id: teamId,
reviewee_team_id: teamId,
});
});

// Persist to backend and patch localStorage with the real DB id
try {
const res = await axiosClient.post('/response_maps', {
assignment_id: assignmentId,
reviewer_user_id: reviewerUserId,
reviewee_team_id: teamId,
});
const realMapId: number = res.data.id;
const realParticipantId: number = res.data.reviewer_id;

if (localMapId !== null) {
mutate(p => {
const map = p.response_maps.find(m => m.id === localMapId);
if (map) {
map.id = realMapId;
map.reviewer_id = realParticipantId;
}
p.responses.forEach(r => { if (r.map_id === localMapId!) r.map_id = realMapId; });
});
}
} catch (err) {
console.warn('Failed to persist response map to backend — local ID will be used:', err);
}
}

function onDeleteReviewer(_teamId: number, mappingId: number) {
Expand All @@ -429,6 +459,8 @@ const AssignReviewer: React.FC = () => {
p.response_maps = p.response_maps.filter(m => m.id !== mappingId);
p.responses = p.responses.filter(r => r.map_id !== mappingId);
});
// Also delete from backend DB
axiosClient.delete(`/response_maps/${mappingId}`).catch(() => {});
}

function onUnsubmit(_teamId: number, mappingId: number) {
Expand All @@ -448,9 +480,20 @@ const AssignReviewer: React.FC = () => {
);
p.response_maps = p.response_maps.filter(m => !ids.has(m.id));
p.responses = p.responses.filter(r => !ids.has(r.map_id));
// Also delete from backend DB
ids.forEach(id => axiosClient.delete(`/response_maps/${id}`).catch(() => {}));
});
}

// E2619: navigate to the questionnaire editor pre-filled as Quiz type.
// The editor will call PATCH /teams/:team_id/quiz_questionnaire after saving and then
// redirect back here via the return_to param.
function onCreateQuiz(teamId: Id) {
if (!hasValidId) return;
const returnTo = encodeURIComponent(`/assignments/edit/${assignmentId}/assignreviewer`);
navigate(`/questionnaires/new?type=Quiz&team_id=${teamId}&assignment_id=${assignmentId}&return_to=${returnTo}`);
}
Comment on lines +491 to +495
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

edit quiz currently routes to create flow.

Line 607 advertises edit behavior, but Line 494 always sends users to /questionnaires/new. Existing team quizzes should route to edit mode.

Suggested fix
-  function onCreateQuiz(teamId: Id) {
+  function onCreateQuiz(teamId: Id, quizQuestionnaireId?: Id | null) {
     if (!hasValidId) return;
     const returnTo = encodeURIComponent(`/assignments/edit/${assignmentId}/assignreviewer`);
-    navigate(`/questionnaires/new?type=Quiz&team_id=${teamId}&assignment_id=${assignmentId}&return_to=${returnTo}`);
+    if (quizQuestionnaireId) {
+      navigate(`/questionnaires/${quizQuestionnaireId}/edit?type=Quiz&team_id=${teamId}&assignment_id=${assignmentId}&return_to=${returnTo}`);
+      return;
+    }
+    navigate(`/questionnaires/new?type=Quiz&team_id=${teamId}&assignment_id=${assignmentId}&return_to=${returnTo}`);
   }
- onClick={() => hasValidId && onCreateQuiz(team.id)}
+ onClick={() => hasValidId && onCreateQuiz(team.id, team.quiz_questionnaire_id)}

Also applies to: 601-608

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

In `@src/pages/Assignments/AssignReviewer.tsx` around lines 491 - 495, The
onCreateQuiz function always routes to the create flow (/questionnaires/new)
even when editing an existing team quiz; update the logic so when a
questionnaireId for an existing quiz is present it navigates to the edit route
instead (e.g.
navigate(`/questionnaires/${questionnaireId}/edit?team_id=${teamId}&assignment_id=${assignmentId}&return_to=${returnTo}`)),
and keep the current /questionnaires/new path only for true create actions;
modify the relevant handlers (onCreateQuiz and the analogous block referenced
around lines 601-608) to check for an existing questionnaireId or “isEdit” flag
and choose the correct route accordingly.


const empty = teams.length === 0 && users.length === 0 && participants.length === 0 && response_maps.length === 0;

return (
Expand Down Expand Up @@ -554,6 +597,15 @@ const AssignReviewer: React.FC = () => {
<div className="ex-actions">
<a role="button" className="ex-link" onClick={() => hasValidId && onAddReviewer(team.id)}>add reviewer</a>
<a role="button" className="ex-link" onClick={() => hasValidId && onDeleteAll(team.id)}>delete outstanding reviewers</a>
{/* E2619: Create/Edit Quiz button — submitting team creates the quiz others must pass before reviewing them */}
<a
role="button"
className="ex-link"
style={{ color: team.quiz_questionnaire_id ? '#2c6b2f' : '#7a2c2c' }}
onClick={() => hasValidId && onCreateQuiz(team.id)}
>
{team.quiz_questionnaire_id ? 'edit quiz' : 'create quiz'}
</a>
</div>
</td>

Expand Down
Loading