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) => ( -
- - {/* General Tab */} - -
-
- - - - {courses && ( - ({ - label: course.name, - value: course.id, - }))} - /> - )} -
- - + {(formik) => { + const handleSave = () => { + // Validate sum of weights = 100% + const totalWeight = formik.values.weights?.reduce((acc: number, curr: number) => acc + curr, 0) || 0; + 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/${formik.values.id}`; + method = HttpMethod.PATCH; + } + // to be used to display message when assignment is created + assignmentData.name = formik.values.name; + + console.log("Sending assignment data:", formik.values); + + sendRequest({ + url: url, + method: method, + data: formik.values, + }); + }; + + + return ( + + + {/* General Tab */} + +
+
+ + + + {courses && ( + ({ + label: course.name, + value: course.id, + }))} + /> + )} +
+ + +
+ + +
- - - -
-
- - - - {formik.values.has_teams && ( -
-
- -
-
- -
- )} - - - {formik.values.has_mentors && ( -
- )} - - - {formik.values.has_topics && ( -
- )} - - - - - - - - {/* Topics Tab */} - -
Topics for {assignmentData.name}
- - - - - - - - {/* TODO: Add topics table here */} - -
- New topic | - Import topics | - | - | - Back -
-
- - {/* Rubrics Tab */} - -
- - - - -
- ([ - { - id: i, - title: `Review round ${i + 1}:`, - questionnaire: ['Sample 1', 'Sample 2', 'Sample 3'], - questionnaire_type: 'dropdown', - }, - { - id: i, - title: `Add tag prompts`, - questionnaire_type: 'tag_prompts', - } - ])).flat(), - { - id: formik.values.number_of_review_rounds ?? 0, - title: "Author feedback:", - questionnaire: ['Standard author feedback'], - questionnaire_type: 'dropdown', - }, - { - id: formik.values.number_of_review_rounds ?? 0, - title: "Add tag prompts", - questionnaire_type: 'tag_prompts', - }, - { - id: (formik.values.number_of_review_rounds ?? 0) + 1, - title: "Teammate review:", - questionnaire: ['Review with Github metrics'], - questionnaire_type: 'dropdown', - }, - { - id: (formik.values.number_of_review_rounds ?? 0) + 1, - title: "Add tag prompts", - questionnaire_type: 'tag_prompts', - }, - ]} - columns={[ - { - cell: ({ row }) =>
{row.original.title}
, - accessorKey: "title", header: "", enableSorting: false, enableColumnFilter: false - }, - { - cell: ({ row }) =>
{row.original.questionnaire_type === 'dropdown' && - ({ label: questionnaire, value: questionnaire }))} />} - {row.original.questionnaire_type === 'tag_prompts' && -
-
}
, - accessorKey: "questionnaire", header: "Questionnaire", enableSorting: false, enableColumnFilter: false - }, - { - cell: ({ row }) =>
{row.original.questionnaire_type === 'dropdown' && - <>
%
}
, - accessorKey: `weights`, header: "Weight", enableSorting: false, enableColumnFilter: false - }, - { - cell: ({ row }) => <>{row.original.questionnaire_type === 'dropdown' && - <>
%
}, - accessorKey: "notification_limits", header: "Notification Limit", enableSorting: false, enableColumnFilter: false - }, - ]} - /> - - - - {/* Review Strategy Tab */} - -
-
- - -
- {formik.values.has_topics && ( -
- -
- + + + + {formik.values.has_teams && ( +
+
+ +
+
+ +
+ )} + + + {formik.values.has_mentors && ( +
+ )} + + + {formik.values.has_topics && ( +
+ )} + + + + + + + + {/* Topics Tab */} + +
Topics for {assignmentData.name}
+ +
+ +
+ + + + {/* TODO: Add topics table here */} + +
+ New topic | + Import topics | + | + | + Back
- )} -
- -
- -
- -
- -
- -
- -
- -
-
- - - - - -
- - {/* Due dates Tab */} - -
-
- -
- -
- -
- - - - +
- + {/* Rubrics Tab */} + +
+ + + -
-
+
{ data={[ ...Array.from({ length: formik.values.number_of_review_rounds ?? 0 }, (_, i) => ([ { - id: 2 * i, - deadline_type: `Review ${i + 1}: Submission`, - }, - { - id: 2 * i + 1, - deadline_type: `Review ${i + 1}: Review`, + id: i, + title: `Review round ${i + 1}:`, + questionnaire: ['Sample 1', 'Sample 2', 'Sample 3'], + questionnaire_type: 'dropdown', }, + ...(formik.values.allow_tag_prompts ? [ + { + id: i, + title: `Add tag prompts`, + questionnaire_type: 'tag_prompts', + }, + ] : []), ])).flat(), - ...(formik.values.use_signup_deadline ? [ - { - id: 'signup_deadline', - deadline_type: "Signup deadline", - }, - ] : []), - ...(formik.values.use_drop_topic_deadline ? [ + { + id: formik.values.number_of_review_rounds ?? 0, + title: "Author feedback:", + questionnaire: ['Standard author feedback'], + questionnaire_type: 'dropdown', + }, + ...(formik.values.allow_tag_prompts ? [ { - id: 'drop_topic_deadline', - deadline_type: "Drop topic deadline", + id: formik.values.number_of_review_rounds ?? 0, + title: "Add tag prompts", + questionnaire_type: 'tag_prompts', }, ] : []), - ...(formik.values.use_team_formation_deadline ? [ + { + id: (formik.values.number_of_review_rounds ?? 0) + 1, + title: "Teammate review:", + questionnaire: ['Review with Github metrics'], + questionnaire_type: 'dropdown', + }, + ...(formik.values.allow_tag_prompts ? [ { - id: 'team_formation_deadline', - deadline_type: "Team formation deadline", + id: (formik.values.number_of_review_rounds ?? 0) + 1, + title: "Add tag prompts", + questionnaire_type: 'tag_prompts', }, ] : []), ]} columns={[ - { accessorKey: "deadline_type", header: "Deadline type", enableSorting: false, enableColumnFilter: false }, - { - cell: ({ row }) => <>, - accessorKey: "date_time", header: "Date & Time", enableSorting: false, enableColumnFilter: false - }, { - cell: ({ row }) => <>, - accessorKey: `use_date_updater`, header: "Use date updater?", enableSorting: false, enableColumnFilter: false + cell: ({ row }) =>
{row.original.title}
, + accessorKey: "title", header: "", enableSorting: false, enableColumnFilter: false }, { - cell: ({ row }) => <> - - , - accessorKey: "submission_allowed", header: "Submission allowed?", enableSorting: false, enableColumnFilter: false + cell: ({ row }) =>
{row.original.questionnaire_type === 'dropdown' && + ({ label: questionnaire, value: questionnaire }))} />} + {row.original.questionnaire_type === 'tag_prompts' && +
+
}
, + accessorKey: "questionnaire", header: "Rubrics", enableSorting: false, enableColumnFilter: false }, { - cell: ({ row }) => <> - - , - accessorKey: "review_allowed", header: "Review allowed?", enableSorting: false, enableColumnFilter: false + cell: ({ row }) =>
{row.original.questionnaire_type === 'dropdown' && + <>
%
}
, + accessorKey: `weights`, header: "Weight", enableSorting: false, enableColumnFilter: false }, { - cell: ({ row }) => <> - - , - accessorKey: "teammate_allowed", header: "Teammate allowed?", enableSorting: false, enableColumnFilter: false - }, - { - cell: ({ row }) => <> - - , - accessorKey: "metareview_allowed", header: "Meta-review allowed?", enableSorting: false, enableColumnFilter: false - }, - { - cell: ({ row }) => <> - , - accessorKey: "reminder", header: "Reminder (hrs)", enableSorting: false, enableColumnFilter: false + cell: ({ row }) => <>{row.original.questionnaire_type === 'dropdown' && + <>
%
}, + accessorKey: "notification_limits", + header: () => ( +
+ Notification limit + +
+ ), + enableSorting: false, + enableColumnFilter: false }, ]} /> - - -
- -
- -
- -
- - - + - {/* Etc Tab */} - -
-
navigate(`participants`)}> - - Add Participant -
-
navigate(`/assignments/edit/${assignmentData.id}/createteams`)}> - - Create Teams + {/* Review Strategy Tab */} + +
+
+
+
-
navigate(`/assignments/edit/${assignmentData.id}/assignreviewer`)}> - - Assign Reviewer + {formik.values.has_topics && ( +
+
+
+ +
+
+ )} +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
-
navigate(`/assignments/edit/${assignmentData.id}/viewsubmissions`)}> - - View Submissions +
+
+
+
+ + + + {/* Due dates Tab */} + +
+
+ +
+ +
+
-
navigate(`/assignments/edit/${assignmentData.id}/viewscores`)}> - - View Scores + + + + + + + + {((formik.values.number_of_review_rounds && formik.values.number_of_review_rounds > 0) || + formik.values.use_signup_deadline || + formik.values.use_drop_topic_deadline || + formik.values.use_team_formation_deadline) && ( + <> +
+
+
([ + { + id: 2 * i, + deadline_type: `Round ${i + 1}: Submission`, + }, + { + id: 2 * i + 1, + deadline_type: `Round ${i + 1}: Review`, + }, + ])).flat(), + ]} + columns={[ + { accessorKey: "deadline_type", header: "Deadline type", enableSorting: false, enableColumnFilter: false }, + { + cell: ({ row }) => <>, + accessorKey: "date_time", header: "Date & Time", enableSorting: false, enableColumnFilter: false + }, + { + cell: ({ row }) => <>, + accessorKey: `use_date_updater`, header: () => ( +
Use date updater?
+ ), enableSorting: false, enableColumnFilter: false + }, + { + cell: ({ row }) => <> + + , + accessorKey: "submission_allowed", header: "Submission allowed?", enableSorting: false, enableColumnFilter: false + }, + { + cell: ({ row }) => <> + + , + accessorKey: "review_allowed", header: "Review allowed?", enableSorting: false, enableColumnFilter: false + }, + { + cell: ({ row }) => <> + + , + accessorKey: "teammate_allowed", header: "Teammate review allowed?", enableSorting: false, enableColumnFilter: false + }, + // { + // cell: ({ row }) => <> + // + // , + // accessorKey: "metareview_allowed", header: "Meta-review allowed?", enableSorting: false, enableColumnFilter: false + // }, + { + cell: ({ row }) => <> + , + accessorKey: "reminder", header: () => ( +
Reminder (hrs):
+ ), enableSorting: false, enableColumnFilter: false + }, + ]} + /> + + + + )} + +
+ +
+ +
+
-
navigate(`/assignments/edit/${assignmentData.id}/viewreports`)}> - - View Reports + + + + + {/* Calibration Tab */} + +

Submit reviews for calibration

+
+
+
({ + id: calibrationSubmission.id, + participant_name: calibrationSubmission.participant_name, + review_status: calibrationSubmission.review_status, + submitted_content: calibrationSubmission.submitted_content, + })), + ]} + columns={[ + { + accessorKey: "participant_name", header: "Participant name", enableSorting: false, enableColumnFilter: false + }, + { + cell: ({ row }) => { + if (row.original.review_status === "not_started") { + return Begin; + } else { + return
+ View + | + Edit +
; + } + }, + accessorKey: "action", header: "Action", enableSorting: false, enableColumnFilter: false + }, + { + cell: ({ row }) => <> +
Hyperlinks:
+
+ { + row.original.submitted_content.hyperlinks.map((item: any, index: number) => { + return {item}; + }) + } +
+
Files:
+
+ { + row.original.submitted_content.files.map((item: any, index: number) => { + return {item}; + }) + } +
+ , + accessorKey: "submitted_content", header: "Submitted items(s)", enableSorting: false, enableColumnFilter: false + }, + ]} + /> + -
navigate(`/assignments/edit/${assignmentData.id}/viewdelayedjobs`)}> - - View Delayed Jobs + + + {/* Etc Tab */} + +
+
navigate(`participants`)}> + User Icon + Add Participant +
+
navigate(`/assignments/edit/${assignmentData.id}/createteams`)}> + User Icon + Create Teams +
+
navigate(`/assignments/edit/${assignmentData.id}/assignreviewer`)}> + User Icon + Assign Reviewer +
+
navigate(`/assignments/edit/${assignmentData.id}/viewsubmissions`)}> + User Icon + View Submissions +
+
navigate(`/assignments/edit/${assignmentData.id}/viewscores`)}> + User Icon + View Scores +
+
navigate(`/assignments/edit/${assignmentData.id}/viewreports`)}> + User Icon + View Reports +
+
navigate(`/assignments/edit/${assignmentData.id}/viewdelayedjobs`)}> + User Icon + View Delayed Jobs +
-
- - - - {/* Submit button */} -
- | - Back -
- - )} + + + + {/* Submit button */} +
+ | + Back +
+ + ); + }} ); - return ( - - - {mode === "update" ? `Update Assignment - ${assignmentData.name}` : "Create Assignment"} - - - {assignmentError &&

{assignmentError}

} - - - - {(formik) => { - return ( -
- - - - - - - - - - {formik.values.has_teams && ( - - )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); - }} -
-
- -
-
navigate(`participants`)}> - - Add Participant -
-
navigate(`/assignments/edit/${assignmentData.id}/createteams`)}> - - Create Teams -
- -
navigate(`/assignments/edit/${assignmentData.id}/assignreviewer`)}> - - Assign Reviewer -
-
navigate(`/assignments/edit/${assignmentData.id}/viewsubmissions`)}> - - View Submissions -
-
navigate(`/assignments/edit/${assignmentData.id}/viewscores`)}> - - View Scores -
-
navigate(`/assignments/edit/${assignmentData.id}/viewreports`)}> - - View Reports -
-
navigate(`/assignments/edit/${assignmentData.id}/viewdelayedjobs`)}> - - View Delayed Jobs -
-
- -
-
-
-
- ); }; export default AssignmentEditor; diff --git a/src/pages/Assignments/AssignmentUtil.ts b/src/pages/Assignments/AssignmentUtil.ts index 2b59301a..2ea98b48 100644 --- a/src/pages/Assignments/AssignmentUtil.ts +++ b/src/pages/Assignments/AssignmentUtil.ts @@ -1,8 +1,8 @@ -import { IAssignmentRequest, IAssignmentResponse } from "../../utils/interfaces"; import axiosClient from "../../utils/axios_client"; export interface IAssignmentFormValues { id?: number; + instructor_id: number; name: string; directory_path: string; spec_location: string; @@ -12,12 +12,14 @@ export interface IAssignmentFormValues { has_badge: boolean; staggered_deadline: boolean; is_calibrated: boolean; + // Teams / mentors / topics has_teams?: boolean; max_team_size?: number; show_teammate_review?: boolean; is_pair_programming?: boolean; has_mentors?: boolean; has_topics?: boolean; + // Review strategy / limits review_topic_threshold?: number; maximum_number_of_reviews_per_submission?: number; review_strategy?: string; @@ -32,66 +34,51 @@ export interface IAssignmentFormValues { allow_self_reviews?: boolean; reviews_visible_to_other_reviewers?: boolean; number_of_review_rounds?: number; + // Dates / penalties days_between_submissions?: number; late_policy_id?: number; is_penalty_calculated?: boolean; calculate_penalty?: boolean; + apply_late_policy?: boolean; + // Deadline toggles use_signup_deadline?: boolean; use_drop_topic_deadline?: boolean; use_team_formation_deadline?: boolean; + // Rubric weights / notification limits weights?: number[]; notification_limits?: number[]; use_date_updater?: boolean[]; + // Per-deadline permissions submission_allowed?: boolean[]; review_allowed?: boolean[]; teammate_allowed?: boolean[]; metareview_allowed?: boolean[]; reminder?: number[]; + // Misc flags from the form + allow_tag_prompts?: boolean; + course_id?: number; + has_quizzes?: boolean; + calibration_for_training?: boolean; + available_to_students?: boolean; + allow_topic_suggestion_from_students?: boolean; + enable_bidding_for_topics?: boolean; + enable_bidding_for_reviews?: boolean; + enable_authors_to_review_other_topics?: boolean; + allow_reviewer_to_choose_topic_to_review?: boolean; + allow_participants_to_create_bookmarks?: boolean; + auto_assign_mentors?: boolean; + staggered_deadline_assignment?: boolean; + // These are used only in tables; keep them loose + questionnaire?: any; + date_time?: any[]; } -export const transformAssignmentRequest = (values: IAssignmentFormValues) => { - const assignment: IAssignmentRequest = { - name: values.name, - directory_path: values.directory_path, - spec_location: values.spec_location, - private: values.private, - show_template_review: values.show_template_review, - require_quiz: values.require_quiz, - has_badge: values.has_badge, - staggered_deadline: values.staggered_deadline, - is_calibrated: values.is_calibrated, - - }; - console.log(assignment); - return JSON.stringify(assignment); -}; - -export const transformAssignmentResponse = (assignmentResponse: string) => { - const assignment: IAssignmentResponse = JSON.parse(assignmentResponse); - const assignmentValues: IAssignmentFormValues = { - id: assignment.id, - name: assignment.name, - directory_path: assignment.directory_path, - spec_location: assignment.spec_location, - private: assignment.private, - show_template_review: assignment.show_template_review, - require_quiz: assignment.require_quiz, - has_badge: assignment.has_badge, - staggered_deadline: assignment.staggered_deadline, - is_calibrated: assignment.is_calibrated, - - }; - return assignmentValues; -}; - export async function loadAssignment({ params }: any) { let assignmentData = {}; // if params contains id, then we are editing a user, so we need to load the user data if (params.id) { - const userResponse = await axiosClient.get(`/assignments/${params.id}`, { - transformResponse: transformAssignmentResponse, - }); + const userResponse = await axiosClient.get(`/assignments/${params.id}`); assignmentData = await userResponse.data; } diff --git a/src/pages/Authentication/Login.tsx b/src/pages/Authentication/Login.tsx index c99cb37f..2051b297 100644 --- a/src/pages/Authentication/Login.tsx +++ b/src/pages/Authentication/Login.tsx @@ -30,7 +30,7 @@ const Login: React.FC = () => { const onSubmit = (values: ILoginFormValues, submitProps: FormikHelpers) => { axios - .post("http://152.7.177.187:3002/login", values) + .post("http://localhost:3002/login", values) .then((response) => { const payload = setAuthToken(response.data.token);