diff --git a/src/App.tsx b/src/App.tsx
index a25e5552..a863e6ec 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,46 +1,46 @@
+import React from "react";
import { createBrowserRouter, Navigate, RouterProvider } from "react-router-dom";
import AdministratorLayout from "./layout/Administrator";
-import RootLayout from "./layout/Root";
import ManageUserTypes, { loader as loadUsers } from "./pages/Administrator/ManageUserTypes";
-import Assignment from "./pages/Assignments/Assignment";
-import AssignmentEditor from "./pages/Assignments/AssignmentEditor";
-import { loadAssignment } from "./pages/Assignments/AssignmentUtil";
-import ResponseMappings from "./pages/ResponseMappings/ResponseMappings";
-import CreateTeams from "./pages/Assignments/CreateTeams";
-import ViewDelayedJobs from "./pages/Assignments/ViewDelayedJobs";
-import ViewReports from "./pages/Assignments/ViewReports";
-import ViewScores from "./pages/Assignments/ViewScores";
-import ViewSubmissions from "./pages/Assignments/ViewSubmissions";
import Login from "./pages/Authentication/Login";
import Logout from "./pages/Authentication/Logout";
-import Courses from "./pages/Courses/Course";
-import CourseEditor from "./pages/Courses/CourseEditor";
-import { loadCourseInstructorDataAndInstitutions } from "./pages/Courses/CourseUtil";
-import Questionnaire from "./pages/EditQuestionnaire/Questionnaire";
-import Email_the_author from "./pages/Email_the_author/email_the_author";
-import Home from "./pages/Home";
import InstitutionEditor, { loadInstitution } from "./pages/Institutions/InstitutionEditor";
import Institutions, { loadInstitutions } from "./pages/Institutions/Institutions";
+import RoleEditor, { loadAvailableRole } from "./pages/Roles/RoleEditor";
+import Roles, { loadRoles } from "./pages/Roles/Roles";
+import Assignment from "./pages/Assignments/Assignment";
+import AssignmentEditor from "./pages/Assignments/AssignmentEditor";
+import { loadAssignment } from "./pages/Assignments/AssignmentUtil";
+import ErrorPage from "./router/ErrorPage";
+import ProtectedRoute from "./router/ProtectedRoute";
+import { ROLE } from "./utils/interfaces";
+import NotFound from "./router/NotFound";
import Participants from "./pages/Participants/Participant";
import ParticipantEditor from "./pages/Participants/ParticipantEditor";
-import ParticipantsAPI from "./pages/Participants/ParticipantsAPI";
-import ParticipantsDemo from "./pages/Participants/ParticipantsDemo";
import { loadParticipantDataRolesAndInstitutions } from "./pages/Participants/participantUtil";
-import EditProfile from "./pages/Profile/Edit";
-import Reviews from "./pages/Reviews/reviews";
-import RoleEditor, { loadAvailableRole } from "./pages/Roles/RoleEditor";
-import Roles, { loadRoles } from "./pages/Roles/Roles";
+import RootLayout from "./layout/Root";
+import UserEditor from "./pages/Users/UserEditor";
+import Users from "./pages/Users/User";
+import { loadUserDataRolesAndInstitutions } from "./pages/Users/userUtil";
+import Home from "./pages/Home";
+import Questionnaire from "./pages/EditQuestionnaire/Questionnaire";
+import Courses from "./pages/Courses/Course";
+import CourseEditor from "./pages/Courses/CourseEditor";
+import { loadCourseInstructorDataAndInstitutions } from "./pages/Courses/CourseUtil";
import TA from "./pages/TA/TA";
import TAEditor from "./pages/TA/TAEditor";
import { loadTAs } from "./pages/TA/TAUtil";
-import Users from "./pages/Users/User";
-import UserEditor from "./pages/Users/UserEditor";
-import { loadUserDataRolesAndInstitutions } from "./pages/Users/userUtil";
import ReviewTable from "./pages/ViewTeamGrades/ReviewTable";
-import ErrorPage from "./router/ErrorPage";
-import NotFound from "./router/NotFound";
-import ProtectedRoute from "./router/ProtectedRoute";
-import { ROLE } from "./utils/interfaces";
+import EditProfile from "./pages/Profile/Edit";
+import Reviews from "./pages/Reviews/reviews";
+import Email_the_author from "./pages/Email_the_author/email_the_author";
+import CreateTeams from "./pages/Assignments/CreateTeams";
+import AssignReviewer from "./pages/Assignments/AssignReviewer";
+import ViewSubmissions from "./pages/Assignments/ViewSubmissions";
+import ViewScores from "./pages/Assignments/ViewScores";
+import ViewReports from "./pages/Assignments/ViewReports";
+import ViewDelayedJobs from "./pages/Assignments/ViewDelayedJobs";
+import ResponseMappings from "./pages/ResponseMappings/ResponseMappings";
function App() {
const router = createBrowserRouter([
{
@@ -51,7 +51,7 @@ function App() {
{ index: true, element: } /> },
{ path: "login", element: },
{ path: "logout", element: } /> },
-
+ // Add the ViewTeamGrades route
{
path: "view-team-grades",
element: } />,
@@ -60,7 +60,6 @@ function App() {
path: "edit-questionnaire",
element: } />,
},
-
{
path: "assignments/edit/:id",
element: ,
@@ -71,13 +70,17 @@ function App() {
element: ,
loader: loadAssignment,
},
-
- // Assign Reviewer: no route loader (component handles localStorage/URL id)
+ // Assign Reviewer: no route loader (component handles localStorage/URL id)
{
path: "assignments/edit/:id/responsemappings",
element: ,
},
+ {
+ path: "assignments/edit/:id/assignreviewer",
+ element: ,
+ loader: loadAssignment,
+ },
{
path: "assignments/edit/:id/viewsubmissions",
element: ,
@@ -98,19 +101,22 @@ function App() {
element: ,
loader: loadAssignment,
},
-
+ {
+ path: "assignments/new",
+ element: ,
+ loader: loadAssignment,
+ },
{
path: "assignments",
element: } leastPrivilegeRole={ROLE.TA} />,
- children: [
- {
- path: "new",
- element: ,
- loader: loadAssignment,
- },
- ],
+ // children: [
+ // {
+ // path: "new",
+ // element: ,
+ // loader: loadAssignment,
+ // },
+ // ],
},
-
{
path: "users",
element: } leastPrivilegeRole={ROLE.TA} />,
@@ -127,7 +133,6 @@ function App() {
},
],
},
-
{
path: "student_tasks/participants",
element: ,
@@ -144,12 +149,10 @@ function App() {
},
],
},
-
{
path: "profile",
element: } />,
},
-
{
path: "assignments/edit/:assignmentId/participants",
element: ,
@@ -166,7 +169,6 @@ function App() {
},
],
},
-
{
path: "student_tasks/edit/:assignmentId/participants",
element: ,
@@ -183,7 +185,6 @@ function App() {
},
],
},
-
{
path: "courses/participants",
element: ,
@@ -204,14 +205,6 @@ function App() {
path: "reviews",
element: ,
},
- {
- path: "demo/participants",
- element: ,
- },
- {
- path: "participants",
- element: } />,
- },
{
path: "email_the_author",
element: ,
@@ -244,7 +237,6 @@ function App() {
},
],
},
-
{
path: "administrator",
element: (
@@ -257,7 +249,10 @@ function App() {
element: ,
loader: loadRoles,
children: [
- { path: "new", element: },
+ {
+ path: "new",
+ element: ,
+ },
{
id: "edit-role",
path: "edit/:id",
@@ -271,7 +266,10 @@ function App() {
element: ,
loader: loadInstitutions,
children: [
- { path: "new", element: },
+ {
+ path: "new",
+ element: ,
+ },
{
path: "edit/:id",
element: ,
@@ -284,16 +282,25 @@ function App() {
element: ,
loader: loadUsers,
children: [
- { path: "new", element: },
- { path: "edit/:id", element: },
+ {
+ path: "new",
+ element: ,
+ },
+
+ {
+ path: "edit/:id",
+ element: ,
+ },
],
},
- { path: "questionnaire", element: },
+ {
+ path: "questionnaire",
+ element: ,
+ },
],
},
-
{ path: "*", element: },
- { path: "questionnaire", element: },
+ { path: "questionnaire", element: }, // Added the Questionnaire route
],
},
]);
@@ -301,4 +308,4 @@ function App() {
return ;
}
-export default App;
\ No newline at end of file
+export default App;
diff --git a/src/custom.scss b/src/custom.scss
index fedacf1d..a68c01f6 100644
--- a/src/custom.scss
+++ b/src/custom.scss
@@ -85,5 +85,23 @@ $theme-colors: (
margin-bottom: 10px; // Space between stacked buttons
}
}
+
+// Custom styles for assignment tabs
+#assignment-tabs {
+ .nav-link {
+ color: #333; // Default tab text color
+ transition: color 0.2s ease;
+
+ &:hover {
+ color: #fff; // Hover text color (you can change this to any color)
+ }
+
+ &.active {
+ color: #333; // Active tab text color (you can change this to any color)
+ font-weight: 600; // Optional: make active tab bold
+ }
+ }
+}
+
// import bootstrap styles at the bottom!
-@import 'bootstrap/scss/bootstrap.scss';
\ No newline at end of file
+@import 'bootstrap/scss/bootstrap.scss';
diff --git a/src/pages/Assignments/AssignmentEditor.test.tsx b/src/pages/Assignments/AssignmentEditor.test.tsx
new file mode 100644
index 00000000..2705914c
--- /dev/null
+++ b/src/pages/Assignments/AssignmentEditor.test.tsx
@@ -0,0 +1,143 @@
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { Provider } from 'react-redux';
+import { BrowserRouter } from 'react-router-dom';
+import { configureStore } from '@reduxjs/toolkit';
+import AssignmentEditor from './AssignmentEditor';
+import authReducer from '../../store/slices/authSlice';
+import alertReducer from '../../store/slices/alertSlice';
+
+// Mock dependencies
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useLoaderData: () => ({ name: 'Test Assignment', id: 1 }),
+ useNavigate: () => jest.fn(),
+ useLocation: () => ({ state: { from: '/assignments' } }),
+}));
+
+jest.mock('hooks/useAPI', () => ({
+ __esModule: true,
+ default: () => ({
+ data: null,
+ error: null,
+ sendRequest: jest.fn(),
+ }),
+}));
+
+jest.mock('components/Form/FormInput', () => ({
+ __esModule: true,
+ default: ({ name, label }: any) => ,
+}));
+
+jest.mock('components/Form/FormSelect', () => ({
+ __esModule: true,
+ default: ({ name, options }: any) => (
+
+ ),
+}));
+
+jest.mock('components/Form/FormCheckBox', () => ({
+ __esModule: true,
+ default: ({ name, label }: any) => (
+
+ ),
+}));
+
+jest.mock('components/Table/Table', () => ({
+ __esModule: true,
+ default: ({ data }: any) =>
{data?.length}
,
+}));
+
+const createMockStore = () => {
+ return configureStore({
+ reducer: {
+ authentication: authReducer,
+ alert: alertReducer,
+ },
+ preloadedState: {
+ authentication: { isAuthenticated: true, user: null },
+ alert: { show: false, variant: '', message: '' },
+ },
+ });
+};
+
+const renderComponent = (mode: 'create' | 'update' = 'create') => {
+ const store = createMockStore();
+ return render(
+
+
+
+
+
+ );
+};
+
+describe('AssignmentEditor', () => {
+ it('renders without crashing', () => {
+ renderComponent();
+ expect(screen.getByText(/Editing Assignment:/i)).toBeInTheDocument();
+ });
+
+ it('renders all tabs', () => {
+ renderComponent();
+ expect(screen.getByText('General')).toBeInTheDocument();
+ expect(screen.getByText('Topics')).toBeInTheDocument();
+ expect(screen.getByText('Rubrics')).toBeInTheDocument();
+ expect(screen.getByText('Review strategy')).toBeInTheDocument();
+ expect(screen.getByText('Due dates')).toBeInTheDocument();
+ expect(screen.getByText('Etc')).toBeInTheDocument();
+ });
+
+ it('renders form inputs in General tab', () => {
+ renderComponent();
+ expect(screen.getByTestId('input-name')).toBeInTheDocument();
+ expect(screen.getByTestId('input-directory_path')).toBeInTheDocument();
+ expect(screen.getByTestId('input-spec_location')).toBeInTheDocument();
+ });
+
+ it('renders checkboxes in General tab', () => {
+ renderComponent();
+ expect(screen.getByTestId('checkbox-private')).toBeInTheDocument();
+ expect(screen.getByTestId('checkbox-has_teams')).toBeInTheDocument();
+ });
+
+ it('shows team-related checkboxes when has_teams is checked', async () => {
+ renderComponent();
+ const hasTeamsCheckbox = screen.getByTestId('checkbox-has_teams');
+ await userEvent.click(hasTeamsCheckbox);
+ await waitFor(() => {
+ expect(screen.getByTestId('checkbox-show_teammate_review')).toBeInTheDocument();
+ });
+ });
+
+
+ it('renders Review strategy tab with select', () => {
+ renderComponent();
+ const reviewStrategyTab = screen.getByText('Review strategy');
+ fireEvent.click(reviewStrategyTab);
+ expect(screen.getByTestId('select-review_strategy')).toBeInTheDocument();
+ });
+
+ it('renders Due dates tab with number input', () => {
+ renderComponent();
+ const dueDatesTab = screen.getByText('Due dates');
+ fireEvent.click(dueDatesTab);
+ expect(screen.getByTestId('input-number_of_review_rounds')).toBeInTheDocument();
+ });
+
+ it('displays submit button', () => {
+ renderComponent();
+ expect(screen.getByText('Save')).toBeInTheDocument();
+ });
+
+ it('renders Back link', () => {
+ renderComponent();
+ expect(screen.getByText('Back')).toBeInTheDocument();
+ });
+});
diff --git a/src/pages/Assignments/AssignmentEditor.tsx b/src/pages/Assignments/AssignmentEditor.tsx
index ac895802..602ccc49 100644
--- a/src/pages/Assignments/AssignmentEditor.tsx
+++ b/src/pages/Assignments/AssignmentEditor.tsx
@@ -6,8 +6,8 @@ import { faClock } from '@fortawesome/free-solid-svg-icons';
import { faFileAlt } from '@fortawesome/free-solid-svg-icons';
import { faChartBar } from '@fortawesome/free-solid-svg-icons';
import { Button, Modal } from "react-bootstrap";
-import { Form, Formik, FormikHelpers } from "formik";
-import { IAssignmentFormValues, transformAssignmentRequest } from "./AssignmentUtil";
+import { Form, Formik } from "formik";
+import { IAssignmentFormValues } from "./AssignmentUtil";
import { IEditor } from "../../utils/interfaces";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
@@ -18,7 +18,7 @@ import { HttpMethod } from "utils/httpMethods";
import { RootState } from "../../store/store";
import { alertActions } from "../../store/slices/alertSlice";
import useAPI from "../../hooks/useAPI";
-import FormCheckbox from "../../components/Form/FormCheckBox";
+import FormCheckbox from "components/Form/FormCheckBox";
import { Tabs, Tab } from 'react-bootstrap';
import '../../custom.scss';
import { faUsers } from '@fortawesome/free-solid-svg-icons';
@@ -30,6 +30,8 @@ import ToolTip from "components/ToolTip";
const initialValues: IAssignmentFormValues = {
name: "",
directory_path: "",
+ instructor_id: 1,
+ course_id: 1,
// dir: "",
spec_location: "",
private: false,
@@ -61,6 +63,7 @@ const initialValues: IAssignmentFormValues = {
use_signup_deadline: false,
use_drop_topic_deadline: false,
use_team_formation_deadline: false,
+ allow_tag_prompts: false,
weights: [],
notification_limits: [],
use_date_updater: [],
@@ -80,12 +83,35 @@ const validationSchema = Yup.object({
const AssignmentEditor = ({ mode }: { mode: "create" | "update" }) => {
const { data: assignmentResponse, error: assignmentError, sendRequest } = useAPI();
const { data: coursesResponse, error: coursesError, sendRequest: sendCoursesRequest } = useAPI();
+ const { data: calibrationSubmissionsResponse, error: calibrationSubmissionsError, sendRequest: sendCalibrationSubmissionsRequest } = useAPI();
const [courses, setCourses] = useState([]);
+ const [calibrationSubmissions, setCalibrationSubmissions] = useState([]);
const auth = useSelector(
(state: RootState) => state.authentication,
(prev, next) => prev.isAuthenticated === next.isAuthenticated
);
const assignmentData: any = useLoaderData();
+
+ // Merge backend-loaded assignment data with frontend defaults:
+ // for any field that is null/undefined in assignmentData, fall back to initialValues.
+ const getInitialValues = (): IAssignmentFormValues => {
+ if (mode !== "update" || !assignmentData) {
+ return initialValues;
+ }
+
+ const merged: any = { ...assignmentData };
+
+ (Object.keys(initialValues) as (keyof IAssignmentFormValues)[]).forEach(
+ (key) => {
+ const value = merged[key];
+ if (value === null || value === undefined) {
+ merged[key] = initialValues[key];
+ }
+ }
+ );
+
+ return merged as IAssignmentFormValues;
+ };
const dispatch = useDispatch();
const navigate = useNavigate();
const location = useLocation();
@@ -120,6 +146,8 @@ const AssignmentEditor = ({ mode }: { mode: "create" | "update" }) => {
});
}, []);
+
+
// Handle courses response
useEffect(() => {
if (coursesResponse && coursesResponse.status >= 200 && coursesResponse.status < 300) {
@@ -127,279 +155,185 @@ const AssignmentEditor = ({ mode }: { mode: "create" | "update" }) => {
}
}, [coursesResponse]);
+
// Show courses error message
useEffect(() => {
coursesError && dispatch(alertActions.showAlert({ variant: "danger", message: coursesError }));
}, [coursesError, dispatch]);
- const onSubmit = (
- values: IAssignmentFormValues,
- submitProps: FormikHelpers
- ) => {
-
- // validate sum of weights = 100%
- const totalWeight = values.weights?.reduce((acc: number, curr: number) => acc + curr, 0) || 0;
- console.log(totalWeight);
- if (totalWeight !== 100) {
- dispatch(alertActions.showAlert({ variant: "danger", message: "Sum of weights must be 100%" }));
- return;
- }
- let method: HttpMethod = HttpMethod.POST;
- let url: string = "/assignments";
- if (mode === "update") {
- url = `/assignments/${values.id}`;
- method = HttpMethod.PATCH;
+ // Load calibration submissions on component mount
+ useEffect(() => {
+ // sendCalibrationSubmissionsRequest({
+ // url: `/calibration_submissions/get_instructor_calibration_submissions/${assignmentData.id}`,
+ // method: HttpMethod.GET,
+ // });
+ setCalibrationSubmissions([
+ {
+ id: 1,
+ participant_name: "Participant 1",
+ review_status: "not_started",
+ submitted_content: { hyperlinks: ["https://www.google.com"], files: ["file1.txt", "file2.pdf"] },
+ },
+ {
+ id: 2,
+ participant_name: "Participant 2",
+ review_status: "in_progress",
+ submitted_content: { hyperlinks: ["https://www.google.com"], files: ["file1.txt", "file2.pdf"] },
+ },
+ ]);
+ }, []);
+
+ // Handle calibration submissions response
+ useEffect(() => {
+ if (calibrationSubmissionsResponse && calibrationSubmissionsResponse.status >= 200 && calibrationSubmissionsResponse.status < 300) {
+ setCalibrationSubmissions(calibrationSubmissionsResponse.data || []);
}
- // to be used to display message when assignment is created
- assignmentData.name = values.name;
- sendRequest({
- url: url,
- method: method,
- data: values,
- transformRequest: transformAssignmentRequest,
- });
- submitProps.setSubmitting(false);
- };
+ }, [calibrationSubmissionsResponse]);
+
+ // Show calibration submissions error message
+ useEffect(() => {
+ calibrationSubmissionsError && dispatch(alertActions.showAlert({ variant: "danger", message: calibrationSubmissionsError }));
+ }, [calibrationSubmissionsError, dispatch]);
+
const handleClose = () => navigate(location.state?.from ? location.state.from : "/assignments");
return (
-
Editing Assignment: {assignmentData.name}
+ {
+ mode === "update" &&
Editing Assignment: {assignmentData.name}
+ }
+ {
+ mode === "create" &&
Creating Assignment
+ }
+
{ }}
validationSchema={validationSchema}
validateOnChange={false}
- enableReinitialize={true}
+ // enableReinitialize={true}
>
- {(formik) => (
-