From d9d390d933addc9caf117f7f06bd3853a8fbae4e Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Fri, 1 Nov 2024 18:56:31 -0400 Subject: [PATCH 01/10] Implemented OCP and updated interfaces --- src/pages/Courses/Course.tsx | 136 ++++++++++++++++++++++---------- src/pages/Courses/CourseUtil.ts | 41 +++++----- src/utils/interfaces.ts | 5 ++ 3 files changed, 121 insertions(+), 61 deletions(-) diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx index d1e4db04..e2ba9c3d 100644 --- a/src/pages/Courses/Course.tsx +++ b/src/pages/Courses/Course.tsx @@ -2,7 +2,7 @@ import { Row as TRow } from "@tanstack/react-table"; import Table from "components/Table/Table"; import useAPI from "hooks/useAPI"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { Button, Col, Container, Row } from "react-bootstrap"; +import { Button, Col, Container, Row, Tooltip } from "react-bootstrap"; import { RiHealthBookLine } from "react-icons/ri"; import { useDispatch, useSelector } from "react-redux"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; @@ -12,17 +12,22 @@ import { ICourseResponse, ROLE } from "../../utils/interfaces"; import { courseColumns as COURSE_COLUMNS } from "./CourseColumns"; import CopyCourse from "./CourseCopy"; import DeleteCourse from "./CourseDelete"; -import { formatDate, mergeDataAndNames } from "./CourseUtil"; +import { formatDate, mergeDataAndNamesAndInstructors } from "./CourseUtil"; +import { OverlayTrigger } from "react-bootstrap"; + +import { ICourseResponse as ICourse } from "../../utils/interfaces"; // Courses Component: Displays and manages courses, including CRUD operations. /** - * @author Atharva Thorve, on December, 2023 - * @author Mrityunjay Joshi on December, 2023 + @author Suraj Raghu Kumar, on Oct, 2024 + * @author Yuktasree Muppala on Oct, 2024 + * @author Harvardhan Patil on Oct, 2024 */ const Courses = () => { const { error, isLoading, data: CourseResponse, sendRequest: fetchCourses } = useAPI(); - const { data: InstitutionResponse, sendRequest: fetchInstitutions } = useAPI(); + const { data: InstitutionResponse, sendRequest: fetchInstitutions} = useAPI(); + const { data: InstructorResponse, sendRequest: fetchInstructors} = useAPI(); const auth = useSelector( (state: RootState) => state.authentication, (prev, next) => prev.isAuthenticated === next.isAuthenticated @@ -31,7 +36,19 @@ const Courses = () => { const location = useLocation(); const dispatch = useDispatch(); - // State for delete and copy confirmation modals + // show course + const [showDetailsModal, setShowDetailsModal] = useState(false); + const [selectedCourse, setSelectedCourse] = useState(null); + + // Utility function to manage modals, adhering to Open-closed-principle +const showModal = (setModalState: React.Dispatch>, + setData?: (data: ICourse | null) => void, data?: ICourse) => { + if (setData) { + setData(data || null); + } + setModalState(true); +}; +const handleShowDetails = (course: ICourse) => showModal(setShowDetailsModal, setSelectedCourse, course); const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{ visible: boolean; data?: ICourseResponse; @@ -44,19 +61,13 @@ const Courses = () => { useEffect(() => { // ToDo: Fix this API in backend so that it the institution name along with the id. Similar to how it is done in users. - if (!showDeleteConfirmation.visible || !showCopyConfirmation.visible) { + if (!showDeleteConfirmation.visible || !showCopyConfirmation.visible){ fetchCourses({ url: `/courses` }); // ToDo: Remove this API call later after the above ToDo is completed fetchInstitutions({ url: `/institutions` }); + fetchInstructors({ url: `/users` }); } - }, [ - fetchCourses, - fetchInstitutions, - location, - showDeleteConfirmation.visible, - auth.user.id, - showCopyConfirmation.visible, - ]); + }, [fetchCourses, fetchInstitutions,fetchInstructors, location, showDeleteConfirmation.visible, auth.user.id, showCopyConfirmation.visible]); // Error alert for API errors useEffect(() => { @@ -66,10 +77,7 @@ const Courses = () => { }, [error, dispatch]); // Callbacks for handling delete and copy confirmation modals - const onDeleteCourseHandler = useCallback( - () => setShowDeleteConfirmation({ visible: false }), - [] - ); + const onDeleteCourseHandler = useCallback(() => setShowDeleteConfirmation({ visible: false }), []); const onCopyCourseHandler = useCallback(() => setShowCopyConfirmation({ visible: false }), []); @@ -85,8 +93,7 @@ const Courses = () => { ); const onDeleteHandle = useCallback( - (row: TRow) => - setShowDeleteConfirmation({ visible: true, data: row.original }), + (row: TRow) => setShowDeleteConfirmation({ visible: true, data: row.original }), [] ); @@ -94,8 +101,9 @@ const Courses = () => { (row: TRow) => setShowCopyConfirmation({ visible: true, data: row.original }), [] ); - + const tableColumns = useMemo( + () => COURSE_COLUMNS(onEditHandle, onDeleteHandle, onTAHandle, onCopyHandle), [onDeleteHandle, onEditHandle, onTAHandle, onCopyHandle] ); @@ -110,7 +118,12 @@ const Courses = () => { [InstitutionResponse?.data, isLoading] ); - tableData = mergeDataAndNames(tableData, institutionData); + const instructorData = useMemo( + () => (isLoading || !InstructorResponse?.data ? [] : InstructorResponse.data), + [InstructorResponse?.data, isLoading] + ); + + tableData = mergeDataAndNamesAndInstructors(tableData, institutionData, instructorData); const formattedTableData = tableData.map((item: any) => ({ ...item, @@ -118,50 +131,89 @@ const Courses = () => { updated_at: formatDate(item.updated_at), })); - // Render the Courses component + // `auth.user.id` holds the ID of the logged-in user + const loggedInUserId = auth.user.id; + const loggedInUserRole = auth.user.role; + + const visibleCourses = useMemo(() => { + // Show all courses to admin and superadmin roles + if (loggedInUserRole === ROLE.ADMIN.valueOf() || loggedInUserRole === ROLE.SUPER_ADMIN.valueOf()) { + return formattedTableData; + } + // Otherwise, only show courses where the logged-in user is the instructor + return formattedTableData.filter((CourseResponse: { instructor_id: number; }) => CourseResponse.instructor_id === loggedInUserId); + }, [formattedTableData, loggedInUserRole]); + // Render the Courses component + return ( <>
- + -

Manage Courses

+

+ {auth.user.role === ROLE.INSTRUCTOR.valueOf() ? ( + <>Instructed by: {auth.user.full_name} + ) : auth.user.role === ROLE.TA.valueOf() ? ( + <>Assisted by: {auth.user.full_name} + ) : ( + <>Manage Courses + )} +


- - - - {showDeleteConfirmation.visible && ( - - )} - {showCopyConfirmation.visible && ( - - )} + + {showDeleteConfirmation.visible && ( + + )} + {showCopyConfirmation.visible && ( + + )} + + + - ); +); + }; -export default Courses; +export default Courses; \ No newline at end of file diff --git a/src/pages/Courses/CourseUtil.ts b/src/pages/Courses/CourseUtil.ts index 5c546257..0d669752 100644 --- a/src/pages/Courses/CourseUtil.ts +++ b/src/pages/Courses/CourseUtil.ts @@ -1,12 +1,14 @@ import { IFormOption } from "components/Form/interfaces"; import { getPrivilegeFromID, hasAllPrivilegesOf } from "utils/util"; import axiosClient from "../../utils/axios_client"; -import { ICourseRequest, ICourseResponse, IInstitution, IInstitutionResponse, IInstructor, IUserRequest, ROLE } from "../../utils/interfaces"; +import { ICourseRequest, ICourseResponse, IInstitution, IInstitutionResponse,IInstructorResponse, IInstructor, IUserRequest, ROLE } from "../../utils/interfaces"; /** - * @author Atharva Thorve, on December, 2023 - * @author Mrityunjay Joshi, on December, 2023 + * @author Aniket Singh Shaktawat, on March, 2024 + * @author Pankhi Saini on March, 2024 + * @author Siddharth Shah on March, 2024 */ + // Course Utility Functions and Constants // Enumeration for course visibility options @@ -145,21 +147,22 @@ export const formatDate = (dateString: string): string => { return new Intl.DateTimeFormat('en-US', options).format(date); }; -// Function to merge data and names -export const mergeDataAndNames = (data: ICourseResponse[], names: IInstitutionResponse[]): any => { +// Function to merge course data with their respective institution and instructor data +export const mergeDataAndNamesAndInstructors = (data: ICourseResponse[], institutionNames: IInstitutionResponse[], instructorNames: IInstructorResponse[]): any => { return data.map((dataObj) => { - const matchingNameObject = names.find((nameObj) => nameObj.id === dataObj.institution_id); - - if (matchingNameObject) { - return { - ...dataObj, - institution: { - id: matchingNameObject.id, - name: matchingNameObject.name, - }, - }; - } - - return dataObj; + // Merge institution data + const matchingInstitution = institutionNames.find((nameObj) => nameObj.id === dataObj.institution_id); + const institutionData = matchingInstitution ? { id: matchingInstitution.id, name: matchingInstitution.name } : {}; + + // Merge instructor data + const matchingInstructor = instructorNames.find((instructorObj) => instructorObj.id === dataObj.instructor_id); + const instructorData = matchingInstructor ? { id: matchingInstructor.id, name: matchingInstructor.name } : {}; + + // Merge course data with institution and instructor data + return { + ...dataObj, + institution: institutionData, + instructor: instructorData + }; }); -}; \ No newline at end of file +}; diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 213909c9..c1ce8d2c 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -142,6 +142,11 @@ export interface IInstitutionResponse { name: string; } +export interface IInstructorResponse { + id: number; + name: string; +} + export enum ROLE { SUPER_ADMIN = "Super Administrator", ADMIN = "Administrator", From 6cbeeb9d20faf1d3f621d93b3fe39b7ba9b62a11 Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Fri, 1 Nov 2024 19:17:15 -0400 Subject: [PATCH 02/10] Added Copying Animator and ISP --- public/assets/images/assign.png | Bin 0 -> 34178 bytes public/assets/images/paste.png | Bin 0 -> 10216 bytes public/assets/images/pencil.png | Bin 0 -> 9786 bytes public/assets/images/remove.png | Bin 0 -> 16895 bytes src/pages/Courses/CourseColumns.tsx | 158 ++++++++++++++++++++-------- src/pages/Courses/CourseCopy.tsx | 40 ++++--- 6 files changed, 139 insertions(+), 59 deletions(-) create mode 100644 public/assets/images/assign.png create mode 100644 public/assets/images/paste.png create mode 100644 public/assets/images/pencil.png create mode 100644 public/assets/images/remove.png diff --git a/public/assets/images/assign.png b/public/assets/images/assign.png new file mode 100644 index 0000000000000000000000000000000000000000..4f55b8ae383812741218a81a44429d217a5df58c GIT binary patch literal 34178 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 literal 0 HcmV?d00001 diff --git a/public/assets/images/paste.png b/public/assets/images/paste.png new file mode 100644 index 0000000000000000000000000000000000000000..0585df93e3faf378ee78b0f1d629b8bf56541fdb GIT binary patch literal 10216 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 aX}hcf7M_BM;CwZ08mxxih4OQDxBm+d!Zl(5 literal 0 HcmV?d00001 diff --git a/public/assets/images/pencil.png b/public/assets/images/pencil.png new file mode 100644 index 0000000000000000000000000000000000000000..676ff53c0ba478a4271c8b42427bad271f27537c GIT binary patch literal 9786 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 Vy%ao^3KC7wH8pK~uB!FZ{{w8S7&-s| literal 0 HcmV?d00001 diff --git a/src/pages/Courses/CourseColumns.tsx b/src/pages/Courses/CourseColumns.tsx index 25002d6f..c2841084 100644 --- a/src/pages/Courses/CourseColumns.tsx +++ b/src/pages/Courses/CourseColumns.tsx @@ -1,81 +1,147 @@ import { createColumnHelper, Row } from "@tanstack/react-table"; -import { Button } from "react-bootstrap"; -import { BsPencilFill, BsPersonXFill } from "react-icons/bs"; -import { MdContentCopy, MdDelete } from "react-icons/md"; +import { Button, Tooltip, OverlayTrigger, Badge } from "react-bootstrap"; import { ICourseResponse as ICourse } from "../../utils/interfaces"; /** - * @author Atharva Thorve, on December, 2023 - * @author Mrityunjay Joshi on December, 2023 - */ + * Author: Suraj Raghu Kumar on October 27, 2023 + Author: Yuktasree Muppala on October 27, 2023 + Author: Harvardhan Patil on October 27, 2023 + */ -// Course Columns Configuration: Defines the columns for the courses table type Fn = (row: Row) => void; + const columnHelper = createColumnHelper(); -export const courseColumns = (handleEdit: Fn, handleDelete: Fn, handleTA: Fn, handleCopy: Fn) => [ - // Column for the course name + +export const courseColumns = ( + handleEdit: Fn, + handleDelete: Fn, + handleTA: Fn, + handleCopy: Fn, + +) => [ columnHelper.accessor("name", { id: "name", - header: "Name", + header: () => Course Name, + cell: (info) => ( +
+ {info.getValue()} +
+ ), enableSorting: true, enableColumnFilter: true, enableGlobalFilter: false, }), + - // Column for the institution name columnHelper.accessor("institution.name", { id: "institution", - header: "Institution", + header: () => Institution, enableSorting: true, enableMultiSort: true, enableGlobalFilter: false, + cell: (info) => ( +
+ {info.getValue() || Not Available} +
+ ), }), - // Column for the creation date - columnHelper.accessor("created_at", { - header: "Creation Date", + columnHelper.accessor("instructor.name", { + id: "instructor", + header: () => Instructor, enableSorting: true, - enableColumnFilter: false, + enableColumnFilter: true, enableGlobalFilter: false, + cell: ({ row }) => { + const instructor = row.original.instructor; + return ( +
+ + {instructor && instructor.name ? ( + instructor.name + ) : ( + Unassigned + )} + +
+ ); + }, + }), + + columnHelper.accessor("created_at", { + header: () => Creation Date, + enableSorting: true, + enableColumnFilter: true, + enableGlobalFilter: true, + cell: (info) => ( +
+ {new Date(info.getValue()).toLocaleDateString() || N/A} +
+ ), }), - // Column for the last updated date columnHelper.accessor("updated_at", { - header: "Updated Date", + header: () => Updated Date, enableSorting: true, - enableColumnFilter: false, - enableGlobalFilter: false, + enableColumnFilter: true, + enableGlobalFilter: true, + cell: (info) => ( +
+ {new Date(info.getValue()).toLocaleDateString() || N/A} +
+ ), }), - // Actions column with edit, delete, TA, and copy buttons columnHelper.display({ id: "actions", - header: "Actions", + header: () => Actions, cell: ({ row }) => ( - <> - - - - - +
+ Edit Course}> + + + + Delete Course}> + + + + Assign TA}> + + + + Copy Course}> + + +
), }), ]; diff --git a/src/pages/Courses/CourseCopy.tsx b/src/pages/Courses/CourseCopy.tsx index 35e61f16..6fa01dae 100644 --- a/src/pages/Courses/CourseCopy.tsx +++ b/src/pages/Courses/CourseCopy.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { Button, Modal } from "react-bootstrap"; +import { Button, Modal, Spinner, Alert } from "react-bootstrap"; import { useDispatch } from "react-redux"; import { alertActions } from "store/slices/alertSlice"; import { HttpMethod } from "utils/httpMethods"; @@ -7,8 +7,9 @@ import useAPI from "../../hooks/useAPI"; import { ICourseResponse as ICourse } from "../../utils/interfaces"; /** - * @author Atharva Thorve, on December, 2023 - * @author Mrityunjay Joshi on December, 2023 + * @author Suraj Raghu Kumar, on Oct, 2024 + * @author Yuktasree Muppala on Oct, 2024 + * @author Harvardhan Patil on Oct, 2024 */ // CopyCourse Component: Modal for copying a course. @@ -20,17 +21,24 @@ interface ICopyCourse { const CopyCourse: React.FC = ({ courseData, onClose }) => { // State and hook declarations - const { data: copiedCourse, error: courseError, sendRequest: CopyCourse } = useAPI(); + const { data: copiedCourse, error: courseError, sendRequest: copyCourseRequest } = useAPI(); const [show, setShow] = useState(true); + const [isCopying, setIsCopying] = useState(false); // State to track copying process const dispatch = useDispatch(); + const courseId = courseData.id; // Function to initiate the course copy process - const copyHandler = () => - CopyCourse({ url: `/courses/${courseData.id}/copy`, method: HttpMethod.GET }); + const copyHandler = () => { + setIsCopying(true); // Set copying state to true + copyCourseRequest({ url: `/courses/${courseId}/copy`, method: HttpMethod.GET });//Applying Interface Segregation principle to use only courseId instead of the whole object + }; // Show error if any useEffect(() => { - if (courseError) dispatch(alertActions.showAlert({ variant: "danger", message: courseError })); + if (courseError) { + dispatch(alertActions.showAlert({ variant: "danger", message: courseError })); + setIsCopying(false); // Reset copying state on error + } }, [courseError, dispatch]); // Close modal if course is copied @@ -55,21 +63,27 @@ const CopyCourse: React.FC = ({ courseData, onClose }) => { // Render the CopyCourse modal return ( - - + + Copy Course - +

Are you sure you want to copy course {courseData.name}?

+ {isCopying && } + {courseError && {courseError}} {/* Display error message */}
- + -
From fcd3a7534814f9ad2b624f521a5411e51be6ed88 Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Fri, 1 Nov 2024 19:22:24 -0400 Subject: [PATCH 03/10] Added dialog and updated method adhering to LSP --- src/pages/Courses/CourseDelete.tsx | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/pages/Courses/CourseDelete.tsx b/src/pages/Courses/CourseDelete.tsx index d5417697..d8ed2a3f 100644 --- a/src/pages/Courses/CourseDelete.tsx +++ b/src/pages/Courses/CourseDelete.tsx @@ -7,8 +7,9 @@ import useAPI from "../../hooks/useAPI"; import { ICourseResponse as ICourse } from "../../utils/interfaces"; /** - * @author Atharva Thorve, on December, 2023 - * @author Mrityunjay Joshi on December, 2023 + * @author Suraj Raghu Kumar, on Oct, 2024 + * @author Yuktasree Muppala on Oct, 2024 + * @author Harvardhan Patil on Oct, 2024 */ // DeleteCourse Component: Modal for deleting a course @@ -32,18 +33,22 @@ const DeleteCourse: React.FC = ({ courseData, onClose }) => { useEffect(() => { if (courseError) dispatch(alertActions.showAlert({ variant: "danger", message: courseError })); }, [courseError, dispatch]); - + + //Added this method to be called for success and achieve LSP and DRY + const handleDeleteSuccess = () => { + setShow(false); + dispatch( + alertActions.showAlert({ + variant: "success", + message: `Course ${courseData.name} deleted successfully!`, + }) + ); + onClose(); + }; // Close modal if course is deleted useEffect(() => { if (deletedCourse?.status && deletedCourse?.status >= 200 && deletedCourse?.status < 300) { - setShow(false); - dispatch( - alertActions.showAlert({ - variant: "success", - message: `Course ${courseData.name} deleted successfully!`, - }) - ); - onClose(); + handleDeleteSuccess(); } }, [deletedCourse?.status, dispatch, onClose, courseData.name]); From 0349674d640089c6b2260eb04381b72875c028cd Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Fri, 1 Nov 2024 19:31:26 -0400 Subject: [PATCH 04/10] Filtered dropdown by institution and implemented SRP --- src/components/Form/interfaces.ts | 2 + src/pages/Courses/CourseEditor.tsx | 93 ++++++++++++++++++++++-------- 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/src/components/Form/interfaces.ts b/src/components/Form/interfaces.ts index 5300dad4..d589e543 100644 --- a/src/components/Form/interfaces.ts +++ b/src/components/Form/interfaces.ts @@ -16,6 +16,7 @@ export interface IFormProps { tooltipPlacement?: "top" | "right" | "bottom" | "left"; inputGroupPrepend?: ReactNode; inputGroupAppend?: ReactNode; + } export interface IFormOption { @@ -25,6 +26,7 @@ export interface IFormOption { export interface IFormPropsWithOption extends IFormProps { options: IFormOption[]; + onChange?: (event: React.ChangeEvent) => void; } export interface IFormikFieldProps { diff --git a/src/pages/Courses/CourseEditor.tsx b/src/pages/Courses/CourseEditor.tsx index ae6104bb..9f3d6651 100644 --- a/src/pages/Courses/CourseEditor.tsx +++ b/src/pages/Courses/CourseEditor.tsx @@ -3,7 +3,7 @@ import FormInput from "components/Form/FormInput"; import FormSelect from "components/Form/FormSelect"; import { Form, Formik, FormikHelpers } from "formik"; import useAPI from "hooks/useAPI"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { Button, InputGroup, Modal } from "react-bootstrap"; import { useDispatch, useSelector } from "react-redux"; import { useLoaderData, useLocation, useNavigate } from "react-router-dom"; @@ -15,8 +15,9 @@ import { IEditor, ROLE } from "../../utils/interfaces"; import { ICourseFormValues, courseVisibility, noSpacesSpecialCharsQuotes, transformCourseRequest } from "./CourseUtil"; /** - * @author Atharva Thorve, on December, 2023 - * @author Mrityunjay Joshi, on December, 2023 + * @author Suraj Raghu Kumar, on Oct, 2024 + * @author Yuktasree Muppala on Oct, 2024 + * @author Harvardhan Patil on Oct, 2024 */ // CourseEditor Component: Modal for creating or updating a course. @@ -24,7 +25,7 @@ const initialValues: ICourseFormValues = { name: "", directory: "", private: [], - institution_id: -1, + institution_id: 0, instructor_id: -1, info: "", }; @@ -44,37 +45,80 @@ const validationSchema = Yup.object({ }); const CourseEditor: React.FC = ({ mode }) => { - // API hook for making requests const { data: courseResponse, error: courseError, sendRequest } = useAPI(); + const { data: users, sendRequest: fetchusers } = useAPI(); const auth = useSelector( (state: RootState) => state.authentication, (prev, next) => prev.isAuthenticated === next.isAuthenticated ); - const { courseData, institutions, instructors }: any = useLoaderData(); + const { courseData, institutions }: any = useLoaderData(); const dispatch = useDispatch(); const navigate = useNavigate(); const location = useLocation(); - initialValues.institution_id = auth.user.institution_id; + interface IFormOption { + label: string; + value: string; + } + + const [filteredInstructors, setFilteredInstructors] = useState([]); + const [selectedInstitutionId, setSelectedInstitutionId] = useState(null); // New state for selected institution - // Close the modal if the course is updated successfully and navigate to the courses page useEffect(() => { - if (courseResponse && courseResponse.status >= 200 && courseResponse.status < 300) { - dispatch( - alertActions.showAlert({ - variant: "success", - message: `Course ${courseData.name} ${mode}d successfully!`, - }) - ); - navigate(location.state?.from ? location.state.from : "/courses"); - } - }, [dispatch, mode, navigate, courseData.name, courseResponse, location.state?.from]); + fetchusers({url:'/users'}); + }, [fetchusers]); - // Show the error message if the course is not updated successfully + // Filter instructors based on selected institution useEffect(() => { - courseError && dispatch(alertActions.showAlert({ variant: "danger", message: courseError })); - }, [courseError, dispatch]); + + if (users) { + const instructorsList: IFormOption[] = [{ label: 'Select an Instructor', value: '' }]; + console.log('Selected Institution ID:', selectedInstitutionId) + + // Filter by instructors by institution + const onlyInstructors = users.data.filter((user: any) => + (user.role.name === 'Instructor')&& (user.institution.id === selectedInstitutionId)); + console.log('Users:', users.data) + onlyInstructors.forEach((instructor: any) => { + instructorsList.push({ label: instructor.name, value: String(instructor.id) }); + }); + setFilteredInstructors(instructorsList); + + } + }, [users, selectedInstitutionId]); // Re-run this effect when users or selectedInstitutionId changes + + // Handle institution selection change and implement Single Responsibility Principle +const handleInstitutionChange = (event: React.ChangeEvent) => { + const institutionId = Number(event.target.value); + setSelectedInstitutionId(institutionId); +}; +// Success handler for course submission +const handleCourseSuccess = () => { + if (courseResponse && courseResponse.status >= 200 && courseResponse.status < 300) { + dispatch( + alertActions.showAlert({ + variant: "success", + message: `Course ${courseData.name} ${mode}d successfully!`, + }) + ); + navigate(location.state?.from ? location.state.from : "/courses"); + } +}; +// Error handler for course submission +const handleCourseError = () => { + if (courseError) { + dispatch(alertActions.showAlert({ variant: "danger", message: courseError })); + } +}; +// useEffect to monitor success response +useEffect(() => { + handleCourseSuccess(); +}, [courseResponse]); +// useEffect to monitor error response +useEffect(() => { + handleCourseError(); +}, [courseError]); // Function to handle form submission const onSubmit = (values: ICourseFormValues, submitProps: FormikHelpers) => { @@ -112,10 +156,11 @@ const CourseEditor: React.FC = ({ mode }) => { initialValues={mode === "update" ? courseData : initialValues} onSubmit={onSubmit} validationSchema={validationSchema} - validateOnChange={false} + validateOnChange={true} enableReinitialize={true} > {(formik) => { + return (
= ({ mode }) => { inputGroupPrepend={ Institution } + + onChange={handleInstitutionChange} // Add onChange to handle institution selection /> Instructors } From 906eb563c02651034d5ca080185386381b36905b Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Fri, 1 Nov 2024 19:50:30 -0400 Subject: [PATCH 05/10] Added Onchnage to filter instructors and implemented an SRP method --- src/components/Form/FormSelect.tsx | 9 ++++++++- src/components/Form/interfaces.ts | 1 - src/pages/Courses/CourseEditor.tsx | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/Form/FormSelect.tsx b/src/components/Form/FormSelect.tsx index 363c3a62..3bc53d4d 100644 --- a/src/components/Form/FormSelect.tsx +++ b/src/components/Form/FormSelect.tsx @@ -8,7 +8,7 @@ import { IFormikFieldProps, IFormPropsWithOption } from "./interfaces"; * @author Ankur Mundra on May, 2023 */ -const FormSelect: React.FC = (props) => { +const FormSelect: React.FC) => void }> = (props) => { const { as, md, @@ -21,6 +21,7 @@ const FormSelect: React.FC = (props) => { tooltipPlacement, disabled, inputGroupPrepend, + onChange, // Add onChange to props to detect chnage in selected institutions. } = props; const displayLabel = tooltip ? ( @@ -48,6 +49,12 @@ const FormSelect: React.FC = (props) => { disabled={disabled} isInvalid={isInvalid} feedback={form.errors[field.name]} + onChange={(event) => { + field.onChange(event); // Call Formik's onChange + if (onChange) { + onChange(event); // Call the passed onChange if provided + } + }} > {options.map((option) => { return ( diff --git a/src/components/Form/interfaces.ts b/src/components/Form/interfaces.ts index d589e543..72af5ed7 100644 --- a/src/components/Form/interfaces.ts +++ b/src/components/Form/interfaces.ts @@ -16,7 +16,6 @@ export interface IFormProps { tooltipPlacement?: "top" | "right" | "bottom" | "left"; inputGroupPrepend?: ReactNode; inputGroupAppend?: ReactNode; - } export interface IFormOption { diff --git a/src/pages/Courses/CourseEditor.tsx b/src/pages/Courses/CourseEditor.tsx index 9f3d6651..84115e00 100644 --- a/src/pages/Courses/CourseEditor.tsx +++ b/src/pages/Courses/CourseEditor.tsx @@ -88,7 +88,7 @@ const CourseEditor: React.FC = ({ mode }) => { } }, [users, selectedInstitutionId]); // Re-run this effect when users or selectedInstitutionId changes - // Handle institution selection change and implement Single Responsibility Principle + // Handle institution selection change const handleInstitutionChange = (event: React.ChangeEvent) => { const institutionId = Number(event.target.value); setSelectedInstitutionId(institutionId); From 49f20308d7f8d86cc15a0e7f8139a24e297849d6 Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Sat, 30 Nov 2024 23:05:06 -0500 Subject: [PATCH 06/10] UI Fix for Instructor view and Add Course Button --- src/pages/Courses/Course.tsx | 445 +++++++++++++++-------------- src/pages/Courses/CourseEditor.tsx | 256 +++++++++-------- 2 files changed, 367 insertions(+), 334 deletions(-) diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx index e2ba9c3d..8f6b873f 100644 --- a/src/pages/Courses/Course.tsx +++ b/src/pages/Courses/Course.tsx @@ -1,219 +1,226 @@ -import { Row as TRow } from "@tanstack/react-table"; -import Table from "components/Table/Table"; -import useAPI from "hooks/useAPI"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { Button, Col, Container, Row, Tooltip } from "react-bootstrap"; -import { RiHealthBookLine } from "react-icons/ri"; -import { useDispatch, useSelector } from "react-redux"; -import { Outlet, useLocation, useNavigate } from "react-router-dom"; -import { alertActions } from "store/slices/alertSlice"; -import { RootState } from "../../store/store"; -import { ICourseResponse, ROLE } from "../../utils/interfaces"; -import { courseColumns as COURSE_COLUMNS } from "./CourseColumns"; -import CopyCourse from "./CourseCopy"; -import DeleteCourse from "./CourseDelete"; -import { formatDate, mergeDataAndNamesAndInstructors } from "./CourseUtil"; -import { OverlayTrigger } from "react-bootstrap"; - -import { ICourseResponse as ICourse } from "../../utils/interfaces"; - -// Courses Component: Displays and manages courses, including CRUD operations. - -/** - @author Suraj Raghu Kumar, on Oct, 2024 - * @author Yuktasree Muppala on Oct, 2024 - * @author Harvardhan Patil on Oct, 2024 - */ -const Courses = () => { - const { error, isLoading, data: CourseResponse, sendRequest: fetchCourses } = useAPI(); - const { data: InstitutionResponse, sendRequest: fetchInstitutions} = useAPI(); - const { data: InstructorResponse, sendRequest: fetchInstructors} = useAPI(); - const auth = useSelector( - (state: RootState) => state.authentication, - (prev, next) => prev.isAuthenticated === next.isAuthenticated - ); - const navigate = useNavigate(); - const location = useLocation(); - const dispatch = useDispatch(); - - // show course - const [showDetailsModal, setShowDetailsModal] = useState(false); - const [selectedCourse, setSelectedCourse] = useState(null); - - // Utility function to manage modals, adhering to Open-closed-principle -const showModal = (setModalState: React.Dispatch>, - setData?: (data: ICourse | null) => void, data?: ICourse) => { - if (setData) { - setData(data || null); - } - setModalState(true); -}; -const handleShowDetails = (course: ICourse) => showModal(setShowDetailsModal, setSelectedCourse, course); - const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{ - visible: boolean; - data?: ICourseResponse; - }>({ visible: false }); - - const [showCopyConfirmation, setShowCopyConfirmation] = useState<{ - visible: boolean; - data?: ICourseResponse; - }>({ visible: false }); - - useEffect(() => { - // ToDo: Fix this API in backend so that it the institution name along with the id. Similar to how it is done in users. - if (!showDeleteConfirmation.visible || !showCopyConfirmation.visible){ - fetchCourses({ url: `/courses` }); - // ToDo: Remove this API call later after the above ToDo is completed - fetchInstitutions({ url: `/institutions` }); - fetchInstructors({ url: `/users` }); - } - }, [fetchCourses, fetchInstitutions,fetchInstructors, location, showDeleteConfirmation.visible, auth.user.id, showCopyConfirmation.visible]); - - // Error alert for API errors - useEffect(() => { - if (error) { - dispatch(alertActions.showAlert({ variant: "danger", message: error })); - } - }, [error, dispatch]); - - // Callbacks for handling delete and copy confirmation modals - const onDeleteCourseHandler = useCallback(() => setShowDeleteConfirmation({ visible: false }), []); - - const onCopyCourseHandler = useCallback(() => setShowCopyConfirmation({ visible: false }), []); - - // Callbacks for navigation and modal handling - const onEditHandle = useCallback( - (row: TRow) => navigate(`edit/${row.original.id}`), - [navigate] - ); - - const onTAHandle = useCallback( - (row: TRow) => navigate(`${row.original.id}/tas`), - [navigate] - ); - - const onDeleteHandle = useCallback( - (row: TRow) => setShowDeleteConfirmation({ visible: true, data: row.original }), - [] - ); - - const onCopyHandle = useCallback( - (row: TRow) => setShowCopyConfirmation({ visible: true, data: row.original }), - [] - ); - - const tableColumns = useMemo( - - () => COURSE_COLUMNS(onEditHandle, onDeleteHandle, onTAHandle, onCopyHandle), - [onDeleteHandle, onEditHandle, onTAHandle, onCopyHandle] - ); - - let tableData = useMemo( - () => (isLoading || !CourseResponse?.data ? [] : CourseResponse.data), - [CourseResponse?.data, isLoading] - ); - - const institutionData = useMemo( - () => (isLoading || !InstitutionResponse?.data ? [] : InstitutionResponse.data), - [InstitutionResponse?.data, isLoading] - ); - - const instructorData = useMemo( - () => (isLoading || !InstructorResponse?.data ? [] : InstructorResponse.data), - [InstructorResponse?.data, isLoading] - ); - - tableData = mergeDataAndNamesAndInstructors(tableData, institutionData, instructorData); - - const formattedTableData = tableData.map((item: any) => ({ - ...item, - created_at: formatDate(item.created_at), - updated_at: formatDate(item.updated_at), - })); - - // `auth.user.id` holds the ID of the logged-in user - const loggedInUserId = auth.user.id; - const loggedInUserRole = auth.user.role; - - const visibleCourses = useMemo(() => { - // Show all courses to admin and superadmin roles - if (loggedInUserRole === ROLE.ADMIN.valueOf() || loggedInUserRole === ROLE.SUPER_ADMIN.valueOf()) { - return formattedTableData; - } - // Otherwise, only show courses where the logged-in user is the instructor - return formattedTableData.filter((CourseResponse: { instructor_id: number; }) => CourseResponse.instructor_id === loggedInUserId); - }, [formattedTableData, loggedInUserRole]); - - // Render the Courses component - - return ( - <> - -
- - -
-

- {auth.user.role === ROLE.INSTRUCTOR.valueOf() ? ( - <>Instructed by: {auth.user.full_name} - ) : auth.user.role === ROLE.TA.valueOf() ? ( - <>Assisted by: {auth.user.full_name} - ) : ( - <>Manage Courses - )} -

- -
- - - -
- - - - - {showDeleteConfirmation.visible && ( - - )} - {showCopyConfirmation.visible && ( - - )} - - -
- - - - - - -); - -}; - -export default Courses; \ No newline at end of file +import { Row as TRow } from "@tanstack/react-table"; +import Table from "components/Table/Table"; +import useAPI from "hooks/useAPI"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Button, Col, Container, Row } from "react-bootstrap"; +import { RiHealthBookLine } from "react-icons/ri"; +import { useDispatch, useSelector } from "react-redux"; +import { Outlet, useLocation, useNavigate } from "react-router-dom"; +import { alertActions } from "store/slices/alertSlice"; +import { RootState } from "../../store/store"; +import { ICourseResponse, ROLE } from "../../utils/interfaces"; +import { courseColumns as COURSE_COLUMNS } from "./CourseColumns"; +import CopyCourse from "./CourseCopy"; +import DeleteCourse from "./CourseDelete"; +import { formatDate, mergeDataAndNamesAndInstructors } from "./CourseUtil"; + +import { ICourseResponse as ICourse } from "../../utils/interfaces"; + +/** + * Courses Component: Displays and manages courses, including CRUD operations. + */ + +const Courses = () => { + const { error, isLoading, data: CourseResponse, sendRequest: fetchCourses } = useAPI(); + const { data: InstitutionResponse, sendRequest: fetchInstitutions } = useAPI(); + const { data: InstructorResponse, sendRequest: fetchInstructors } = useAPI(); + const auth = useSelector( + (state: RootState) => state.authentication, + (prev, next) => prev.isAuthenticated === next.isAuthenticated + ); + const navigate = useNavigate(); + const location = useLocation(); + const dispatch = useDispatch(); + + // State for course details modal + const [showDetailsModal, setShowDetailsModal] = useState(false); + const [selectedCourse, setSelectedCourse] = useState(null); + + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{ + visible: boolean; + data?: ICourseResponse; + }>({ visible: false }); + + const [showCopyConfirmation, setShowCopyConfirmation] = useState<{ + visible: boolean; + data?: ICourseResponse; + }>({ visible: false }); + + // Utility function to handle modals + const showModal = ( + setModalState: React.Dispatch>, + setData?: (data: ICourse | null) => void, + data?: ICourse + ) => { + if (setData) { + setData(data || null); + } + setModalState(true); + }; + + const handleShowDetails = (course: ICourse) => + showModal(setShowDetailsModal, setSelectedCourse, course); + + useEffect(() => { + // Ensure the API fetch happens unless modals are active + if (!showDeleteConfirmation.visible || !showCopyConfirmation.visible) { + fetchCourses({ url: `/courses` }); + fetchInstitutions({ url: `/institutions` }); + fetchInstructors({ url: `/users` }); + } + }, [ + fetchCourses, + fetchInstitutions, + fetchInstructors, + location, + showDeleteConfirmation.visible, + auth.user.id, + showCopyConfirmation.visible, + ]); + + useEffect(() => { + if (error) { + dispatch(alertActions.showAlert({ variant: "danger", message: error })); + } + }, [error, dispatch]); + + const onDeleteCourseHandler = useCallback( + () => setShowDeleteConfirmation({ visible: false }), + [] + ); + + const onCopyCourseHandler = useCallback( + () => setShowCopyConfirmation({ visible: false }), + [] + ); + + const onEditHandle = useCallback( + (row: TRow) => navigate(`edit/${row.original.id}`), + [navigate] + ); + + const onTAHandle = useCallback( + (row: TRow) => navigate(`${row.original.id}/tas`), + [navigate] + ); + + const onDeleteHandle = useCallback( + (row: TRow) => + setShowDeleteConfirmation({ visible: true, data: row.original }), + [] + ); + + const onCopyHandle = useCallback( + (row: TRow) => + setShowCopyConfirmation({ visible: true, data: row.original }), + [] + ); + + const tableColumns = useMemo( + () => + COURSE_COLUMNS(onEditHandle, onDeleteHandle, onTAHandle, onCopyHandle), + [onDeleteHandle, onEditHandle, onTAHandle, onCopyHandle] + ); + + const tableData = useMemo( + () => (isLoading || !CourseResponse?.data ? [] : CourseResponse.data), + [CourseResponse?.data, isLoading] + ); + + const institutionData = useMemo( + () => (isLoading || !InstitutionResponse?.data ? [] : InstitutionResponse.data), + [InstitutionResponse?.data, isLoading] + ); + + const instructorData = useMemo( + () => (isLoading || !InstructorResponse?.data ? [] : InstructorResponse.data), + [InstructorResponse?.data, isLoading] + ); + + const mergedTableData = useMemo( + () => + mergeDataAndNamesAndInstructors(tableData, institutionData, instructorData).map( + (item: any) => ({ + ...item, + created_at: formatDate(item.created_at), + updated_at: formatDate(item.updated_at), + }) + ), + [tableData, institutionData, instructorData] + ); + + const loggedInUserRole = auth.user.role; + + const visibleCourses = useMemo(() => { + if ( + loggedInUserRole === ROLE.ADMIN.valueOf() || + loggedInUserRole === ROLE.SUPER_ADMIN.valueOf() + ) { + return mergedTableData; + } + return mergedTableData.filter( + (CourseResponse: { instructor_id: number }) => + CourseResponse.instructor_id === auth.user.id + ); + }, [mergedTableData, loggedInUserRole]); + + return ( + <> + +
+ + +
+

+ {auth.user.role === ROLE.INSTRUCTOR.valueOf() ? ( + <>Instructed by: {auth.user.full_name} + ) : auth.user.role === ROLE.TA.valueOf() ? ( + <>Assisted by: {auth.user.full_name} + ) : ( + <>Manage Courses + )} +

+ + + + + +
+ + + + + {showDeleteConfirmation.visible && ( + + )} + {showCopyConfirmation.visible && ( + + )} + + +
+ + + + + ); +}; + +export default Courses; diff --git a/src/pages/Courses/CourseEditor.tsx b/src/pages/Courses/CourseEditor.tsx index 84115e00..908d3ae8 100644 --- a/src/pages/Courses/CourseEditor.tsx +++ b/src/pages/Courses/CourseEditor.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useState } from "react"; import { Button, InputGroup, Modal } from "react-bootstrap"; import { useDispatch, useSelector } from "react-redux"; import { useLoaderData, useLocation, useNavigate } from "react-router-dom"; -import { alertActions } from "store/slices/alertSlice"; +import { alertActions } from "store/slices/alertSlice"; // Success message utility import { HttpMethod } from "utils/httpMethods"; import * as Yup from "yup"; import { RootState } from "../../store/store"; @@ -15,12 +15,11 @@ import { IEditor, ROLE } from "../../utils/interfaces"; import { ICourseFormValues, courseVisibility, noSpacesSpecialCharsQuotes, transformCourseRequest } from "./CourseUtil"; /** - * @author Suraj Raghu Kumar, on Oct, 2024 - * @author Yuktasree Muppala on Oct, 2024 - * @author Harvardhan Patil on Oct, 2024 + * @author Suraj + * @editor Updated for Role-Based Restrictions */ -// CourseEditor Component: Modal for creating or updating a course. +// Initial form values const initialValues: ICourseFormValues = { name: "", directory: "", @@ -45,7 +44,6 @@ const validationSchema = Yup.object({ }); const CourseEditor: React.FC = ({ mode }) => { - // API hook for making requests const { data: courseResponse, error: courseError, sendRequest } = useAPI(); const { data: users, sendRequest: fetchusers } = useAPI(); const auth = useSelector( @@ -63,161 +61,189 @@ const CourseEditor: React.FC = ({ mode }) => { } const [filteredInstructors, setFilteredInstructors] = useState([]); - const [selectedInstitutionId, setSelectedInstitutionId] = useState(null); // New state for selected institution + const [selectedInstitutionId, setSelectedInstitutionId] = useState(null); + // Fetch all users or restrict based on the logged-in role useEffect(() => { - fetchusers({url:'/users'}); - }, [fetchusers]); + if (auth.user.role === ROLE.INSTRUCTOR.valueOf()) { + setSelectedInstitutionId(auth.user.institution_id); + setFilteredInstructors([ + { label: auth.user.full_name, value: String(auth.user.id) }, + ]); + } else { + fetchusers({ url: "/users" }); + } + }, [auth.user, fetchusers]); // Filter instructors based on selected institution useEffect(() => { - if (users) { - const instructorsList: IFormOption[] = [{ label: 'Select an Instructor', value: '' }]; - console.log('Selected Institution ID:', selectedInstitutionId) - - // Filter by instructors by institution - const onlyInstructors = users.data.filter((user: any) => - (user.role.name === 'Instructor')&& (user.institution.id === selectedInstitutionId)); - console.log('Users:', users.data) + const instructorsList: IFormOption[] = [{ label: "Select an Instructor", value: "" }]; + const onlyInstructors = users.data.filter( + (user: any) => + user.role.name === "Instructor" && + user.institution.id === selectedInstitutionId + ); onlyInstructors.forEach((instructor: any) => { - instructorsList.push({ label: instructor.name, value: String(instructor.id) }); + instructorsList.push({ + label: instructor.name, + value: String(instructor.id), + }); }); setFilteredInstructors(instructorsList); - } - }, [users, selectedInstitutionId]); // Re-run this effect when users or selectedInstitutionId changes - - // Handle institution selection change -const handleInstitutionChange = (event: React.ChangeEvent) => { - const institutionId = Number(event.target.value); - setSelectedInstitutionId(institutionId); -}; -// Success handler for course submission -const handleCourseSuccess = () => { - if (courseResponse && courseResponse.status >= 200 && courseResponse.status < 300) { - dispatch( - alertActions.showAlert({ - variant: "success", - message: `Course ${courseData.name} ${mode}d successfully!`, - }) - ); - navigate(location.state?.from ? location.state.from : "/courses"); - } -}; -// Error handler for course submission -const handleCourseError = () => { - if (courseError) { - dispatch(alertActions.showAlert({ variant: "danger", message: courseError })); - } -}; -// useEffect to monitor success response -useEffect(() => { - handleCourseSuccess(); -}, [courseResponse]); -// useEffect to monitor error response -useEffect(() => { - handleCourseError(); -}, [courseError]); - - // Function to handle form submission - const onSubmit = (values: ICourseFormValues, submitProps: FormikHelpers) => { - let method: HttpMethod = HttpMethod.POST; - let url: string = "/courses"; - - if (mode === "update") { - url = `/courses/${values.id}`; - method = HttpMethod.PATCH; + }, [users, selectedInstitutionId]); + + const handleInstitutionChange = (event: React.ChangeEvent) => { + const institutionId = Number(event.target.value); + setSelectedInstitutionId(institutionId); + }; + + // Show success message after course creation + useEffect(() => { + if (courseResponse && courseResponse.status >= 200 && courseResponse.status < 300) { + dispatch( + alertActions.showAlert({ + variant: "success", + message: `Course "${courseResponse.data.name}" created successfully!`, // Display course name + }) + ); + navigate(location.state?.from || "/courses"); // Redirect back to courses } + }, [courseResponse, dispatch, navigate, location.state]); + + const onSubmit = ( + values: ICourseFormValues, + submitProps: FormikHelpers + ) => { + const method = mode === "update" ? HttpMethod.PATCH : HttpMethod.POST; + const url = mode === "update" ? `/courses/${values.id}` : "/courses"; - // to be used to display message when course is created - courseData.name = values.name; sendRequest({ - url: url, - method: method, + url, + method, data: values, transformRequest: transformCourseRequest, }); + submitProps.setSubmitting(false); }; - // Function to close the modal - const handleClose = () => navigate(location.state?.from ? location.state.from : "/courses"); - - // Render the CourseEditor modal return ( - + navigate(location.state?.from || "/courses")} backdrop="static"> {mode === "update" ? "Update Course" : "Create Course"} {courseError &&

{courseError}

} - + initialValues={{ + ...initialValues, + institution_id: auth.user.role === ROLE.INSTRUCTOR.valueOf() ? auth.user.institution_id : initialValues.institution_id, + instructor_id: auth.user.role === ROLE.INSTRUCTOR.valueOf() ? auth.user.id : initialValues.instructor_id, + }} onSubmit={onSubmit} validationSchema={validationSchema} validateOnChange={true} enableReinitialize={true} > - {(formik) => { - - return ( - + {(formik) => ( + + {/* Institution Dropdown */} + {auth.user.role === ROLE.INSTRUCTOR.valueOf() && ( inst.value === auth.user.institution_id + )?.label || "Select Institution", + value: String(auth.user.institution_id), + }, + ]} + inputGroupPrepend={ + Institution + } + /> + )} + {auth.user.role !== ROLE.INSTRUCTOR.valueOf() && ( + Institution } - - onChange={handleInstitutionChange} // Add onChange to handle institution selection + onChange={handleInstitutionChange} /> + )} + + {/* Instructor Dropdown */} + {auth.user.role === ROLE.INSTRUCTOR.valueOf() && ( Instructors + Instructor } /> - Instructors + } /> - - - - - - - - - - - ); - }} + )} + + + + + + + + + + + + )}
From cbd27e610c76242027694f1f816234f1e9ced0f4 Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Sat, 30 Nov 2024 23:27:55 -0500 Subject: [PATCH 07/10] UI Fix for Action Icons --- public/assets/images/Copy-icon-24.png | Bin 0 -> 428 bytes public/assets/images/add-ta-24.png | Bin 0 -> 1491 bytes public/assets/images/delete-icon-24.png | Bin 0 -> 1406 bytes public/assets/images/edit-icon-24.png | Bin 0 -> 1252 bytes src/pages/Courses/CourseColumns.tsx | 331 +++++++++++++----------- 5 files changed, 184 insertions(+), 147 deletions(-) create mode 100644 public/assets/images/Copy-icon-24.png create mode 100644 public/assets/images/add-ta-24.png create mode 100644 public/assets/images/delete-icon-24.png create mode 100644 public/assets/images/edit-icon-24.png diff --git a/public/assets/images/Copy-icon-24.png b/public/assets/images/Copy-icon-24.png new file mode 100644 index 0000000000000000000000000000000000000000..6d4f0eb08f8046a1301fec0974e367d9a181fa0b GIT binary patch literal 428 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 O0000) => void; - -const columnHelper = createColumnHelper(); - -export const courseColumns = ( - handleEdit: Fn, - handleDelete: Fn, - handleTA: Fn, - handleCopy: Fn, - -) => [ - columnHelper.accessor("name", { - id: "name", - header: () => Course Name, - cell: (info) => ( -
- {info.getValue()} -
- ), - enableSorting: true, - enableColumnFilter: true, - enableGlobalFilter: false, - }), - - - columnHelper.accessor("institution.name", { - id: "institution", - header: () => Institution, - enableSorting: true, - enableMultiSort: true, - enableGlobalFilter: false, - cell: (info) => ( -
- {info.getValue() || Not Available} -
- ), - }), - - columnHelper.accessor("instructor.name", { - id: "instructor", - header: () => Instructor, - enableSorting: true, - enableColumnFilter: true, - enableGlobalFilter: false, - cell: ({ row }) => { - const instructor = row.original.instructor; - return ( -
- - {instructor && instructor.name ? ( - instructor.name - ) : ( - Unassigned - )} - -
- ); - }, - }), - - columnHelper.accessor("created_at", { - header: () => Creation Date, - enableSorting: true, - enableColumnFilter: true, - enableGlobalFilter: true, - cell: (info) => ( -
- {new Date(info.getValue()).toLocaleDateString() || N/A} -
- ), - }), - - columnHelper.accessor("updated_at", { - header: () => Updated Date, - enableSorting: true, - enableColumnFilter: true, - enableGlobalFilter: true, - cell: (info) => ( -
- {new Date(info.getValue()).toLocaleDateString() || N/A} -
- ), - }), - - columnHelper.display({ - id: "actions", - header: () => Actions, - cell: ({ row }) => ( -
- Edit Course}> - - - - Delete Course}> - - - - Assign TA}> - - - - Copy Course}> - - -
- ), - }), -]; +import { createColumnHelper, Row } from "@tanstack/react-table"; +import { Button, Tooltip, OverlayTrigger, Badge } from "react-bootstrap"; +import { ICourseResponse as ICourse } from "../../utils/interfaces"; + +/** + * Author: Suraj Raghu Kumar on October 27, 2023 + Author: Yuktasree Muppala on October 27, 2023 + Author: Harvardhan Patil on October 27, 2023 + */ + +type Fn = (row: Row) => void; + +const columnHelper = createColumnHelper(); + +export const courseColumns = ( + handleEdit: Fn, + handleDelete: Fn, + handleTA: Fn, + handleCopy: Fn +) => [ + columnHelper.accessor("name", { + id: "name", + header: () => ( + + Course Name + + ), + cell: (info) => ( +
+ {info.getValue()} +
+ ), + enableSorting: true, + enableColumnFilter: true, + enableGlobalFilter: false, + }), + + columnHelper.accessor("institution.name", { + id: "institution", + header: () => ( + + Institution + + ), + cell: (info) => ( +
+ {info.getValue() || Not Available} +
+ ), + enableSorting: true, + enableMultiSort: true, + enableGlobalFilter: false, + }), + + columnHelper.accessor("instructor.name", { + id: "instructor", + header: () => ( + + Instructor + + ), + cell: ({ row }) => { + const instructor = row.original.instructor; + return ( +
+ + {instructor && instructor.name ? ( + instructor.name + ) : ( + Unassigned + )} + +
+ ); + }, + enableSorting: true, + enableColumnFilter: true, + enableGlobalFilter: false, + }), + + columnHelper.accessor("created_at", { + header: () => ( + + Creation Date + + ), + cell: (info) => ( +
+ {new Date(info.getValue()).toLocaleDateString() || N/A} +
+ ), + enableSorting: true, + enableColumnFilter: true, + enableGlobalFilter: true, + }), + + columnHelper.accessor("updated_at", { + header: () => ( + + Updated Date + + ), + cell: (info) => ( +
+ {new Date(info.getValue()).toLocaleDateString() || N/A} +
+ ), + enableSorting: true, + enableColumnFilter: true, + enableGlobalFilter: true, + }), + + columnHelper.display({ + id: "actions", + header: () => ( + + Actions + + ), + cell: ({ row }) => ( +
+ Edit Course}> + + + + Delete Course}> + + + + Assign TA}> + + + + Copy Course}> + + +
+ ), + }), +]; From ca0e71e357ebc21ff0fb4e7f3dfe161e025d2f0d Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Mon, 2 Dec 2024 16:53:38 -0500 Subject: [PATCH 08/10] UI Fix for text fields prepopulation --- src/pages/Courses/Course.tsx | 2 +- src/pages/Courses/CourseColumns.tsx | 30 ++-- src/pages/Courses/CourseEditor.tsx | 261 ++++++++++++++-------------- src/pages/Courses/CourseUtil.ts | 5 +- 4 files changed, 149 insertions(+), 149 deletions(-) diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx index 8f6b873f..859d2d9e 100644 --- a/src/pages/Courses/Course.tsx +++ b/src/pages/Courses/Course.tsx @@ -205,7 +205,7 @@ const Courses = () => { /> )} - +
[ columnHelper.accessor("name", { id: "name", @@ -35,8 +35,8 @@ export const courseColumns = ( enableGlobalFilter: false, }), - columnHelper.accessor("institution.name", { - id: "institution", + /*columnHelper.accessor("institution.name", { + id: "institution", header: () => ( Institution @@ -50,7 +50,7 @@ export const courseColumns = ( enableSorting: true, enableMultiSort: true, enableGlobalFilter: false, - }), + }),*/ columnHelper.accessor("instructor.name", { id: "instructor", @@ -148,21 +148,13 @@ export const courseColumns = ( /> - - Assign TA}> - - + + Assign TA}> + + + Copy Course}> - - - - )} + + + + + + + + + + + + ); + }} ); }; - -export default CourseEditor; +export default CourseEditor; \ No newline at end of file diff --git a/src/pages/Courses/CourseUtil.ts b/src/pages/Courses/CourseUtil.ts index 0d669752..c676f1b9 100644 --- a/src/pages/Courses/CourseUtil.ts +++ b/src/pages/Courses/CourseUtil.ts @@ -107,8 +107,9 @@ export async function loadCourseInstructorDataAndInstitutions({ params }: any) { transformResponse: transformInstructorResponse, }); const users = await usersResponse.data; - - const instructors = users.filter((user: IUserRequest) => !hasAllPrivilegesOf(getPrivilegeFromID(user.role_id), ROLE.TA)); + console.log(users.role_id) + console.log(courseData) + const instructors = users.filter((user: IUserRequest) => !hasAllPrivilegesOf(getPrivilegeFromID(user.role_id), ROLE.INSTRUCTOR)); return { courseData, institutions, instructors } } From 328f9827c363814061e21554f84801f882d22173 Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Tue, 3 Dec 2024 21:13:49 -0500 Subject: [PATCH 09/10] UI fix for copy and delete modals --- src/components/Table/GlobalFilter.tsx | 11 +++- src/components/Table/Table.tsx | 3 +- src/pages/Courses/Course.tsx | 2 +- src/pages/Courses/CourseColumns.tsx | 94 +++++++++++++++------------ src/pages/Courses/CourseCopy.tsx | 22 +++---- src/pages/Courses/CourseDelete.tsx | 6 +- src/pages/Courses/CourseEditor.tsx | 41 +++++++----- 7 files changed, 106 insertions(+), 73 deletions(-) diff --git a/src/components/Table/GlobalFilter.tsx b/src/components/Table/GlobalFilter.tsx index 2cc9c7ac..6dd73fc6 100644 --- a/src/components/Table/GlobalFilter.tsx +++ b/src/components/Table/GlobalFilter.tsx @@ -8,14 +8,23 @@ import DebouncedInput from "./DebouncedInput"; interface FilterProps { filterValue: string | number; setFilterValue: (value: string | number) => void; + isDisabled?: boolean; // New optional prop to disable the filter } -const GlobalFilter: React.FC = ({ filterValue, setFilterValue }) => { +const GlobalFilter: React.FC = ({ + filterValue, + setFilterValue, + isDisabled = true, // Default to true for disabling +}) => { const searchHandler = useCallback( (value: string | number) => setFilterValue(value), [setFilterValue] ); + if (isDisabled) { + return null; // Render nothing when disabled + } + return ( = ({ )} - - {isGlobalFilterVisible ? " Hide" : " Show"} + {" "} diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx index 859d2d9e..29f402de 100644 --- a/src/pages/Courses/Course.tsx +++ b/src/pages/Courses/Course.tsx @@ -223,4 +223,4 @@ const Courses = () => { ); }; -export default Courses; +export default Courses; \ No newline at end of file diff --git a/src/pages/Courses/CourseColumns.tsx b/src/pages/Courses/CourseColumns.tsx index 33e42ccb..8f75ca79 100644 --- a/src/pages/Courses/CourseColumns.tsx +++ b/src/pages/Courses/CourseColumns.tsx @@ -16,53 +16,42 @@ export const courseColumns = ( handleEdit: Fn, handleDelete: Fn, handleTA: Fn, - handleCopy: Fn, + handleCopy: Fn ) => [ columnHelper.accessor("name", { id: "name", header: () => ( - + Course Name ), cell: (info) => ( -
+
{info.getValue()}
), enableSorting: true, enableColumnFilter: true, - enableGlobalFilter: false, + enableGlobalFilter: true, }), - /*columnHelper.accessor("institution.name", { - id: "institution", - header: () => ( - - Institution - - ), - cell: (info) => ( -
- {info.getValue() || Not Available} -
- ), - enableSorting: true, - enableMultiSort: true, - enableGlobalFilter: false, - }),*/ - columnHelper.accessor("instructor.name", { id: "instructor", header: () => ( - + Instructor ), cell: ({ row }) => { const instructor = row.original.instructor; return ( -
+
{instructor && instructor.name ? ( instructor.name @@ -75,18 +64,25 @@ export const courseColumns = ( }, enableSorting: true, enableColumnFilter: true, - enableGlobalFilter: false, + enableGlobalFilter: true, }), columnHelper.accessor("created_at", { header: () => ( - + Creation Date ), cell: (info) => ( -
- {new Date(info.getValue()).toLocaleDateString() || N/A} +
+ + {new Date(info.getValue()).toLocaleDateString() || ( + N/A + )} +
), enableSorting: true, @@ -96,29 +92,39 @@ export const courseColumns = ( columnHelper.accessor("updated_at", { header: () => ( - + Updated Date ), cell: (info) => ( -
- {new Date(info.getValue()).toLocaleDateString() || N/A} +
+ + {new Date(info.getValue()).toLocaleDateString() || ( + N/A + )} +
), enableSorting: true, enableColumnFilter: true, - enableGlobalFilter: true, + enableGlobalFilter: false, }), columnHelper.display({ id: "actions", header: () => ( - + Actions ), cell: ({ row }) => ( -
+
Edit Course}> - - Assign TA}> - - - + + Assign TA}> + + Copy Course}> - @@ -90,4 +89,5 @@ const CopyCourse: React.FC = ({ courseData, onClose }) => { ); }; -export default CopyCourse; + +export default CopyCourse; \ No newline at end of file diff --git a/src/pages/Courses/CourseDelete.tsx b/src/pages/Courses/CourseDelete.tsx index d8ed2a3f..ddb6ea04 100644 --- a/src/pages/Courses/CourseDelete.tsx +++ b/src/pages/Courses/CourseDelete.tsx @@ -34,7 +34,7 @@ const DeleteCourse: React.FC = ({ courseData, onClose }) => { if (courseError) dispatch(alertActions.showAlert({ variant: "danger", message: courseError })); }, [courseError, dispatch]); - //Added this method to be called for success and achieve LSP and DRY + //Added this method to be called in below and achieve LSP const handleDeleteSuccess = () => { setShow(false); dispatch( @@ -60,7 +60,7 @@ const DeleteCourse: React.FC = ({ courseData, onClose }) => { // Render the DeleteCourse modal return ( - + Delete Course @@ -81,4 +81,4 @@ const DeleteCourse: React.FC = ({ courseData, onClose }) => { ); }; -export default DeleteCourse; +export default DeleteCourse; \ No newline at end of file diff --git a/src/pages/Courses/CourseEditor.tsx b/src/pages/Courses/CourseEditor.tsx index ec697a68..c02a12da 100644 --- a/src/pages/Courses/CourseEditor.tsx +++ b/src/pages/Courses/CourseEditor.tsx @@ -56,7 +56,7 @@ const CourseEditor: React.FC = ({ mode }) => { const dispatch = useDispatch(); const navigate = useNavigate(); const location = useLocation(); - + console.log(courseData) interface IFormOption { label: string; value: string; @@ -70,16 +70,14 @@ const CourseEditor: React.FC = ({ mode }) => { if (auth.user.role === ROLE.INSTRUCTOR.valueOf()) { setSelectedInstitutionId(auth.user.institution_id); setFilteredInstructors([ - { label: auth.user.full_name, value: String(auth.user.id) }, + { label: auth.user.name, value: String(auth.user.id) }, ]); } else { fetchusers({ url: "/users" }); } }, [auth.user, fetchusers]); - /*useEffect(() => { - fetchusers({url:'/users'}); - }, [fetchusers]);*/ - + + // Filter instructors based on selected institution useEffect(() => { @@ -89,14 +87,16 @@ const CourseEditor: React.FC = ({ mode }) => { // Filter by instructors by institution const onlyInstructors = users.data.filter((user: any) => (user.role.name === 'Instructor')&& (user.institution.id === selectedInstitutionId)); - console.log('Users:', users.data) + //console.log('Users:', users.data) onlyInstructors.forEach((instructor: any) => { instructorsList.push({ label: instructor.name, value: String(instructor.id) }); }); + setFilteredInstructors(instructorsList); } }, [users, selectedInstitutionId]); // Re-run this effect when users or selectedInstitutionId changes + // Handle institution selection change const handleInstitutionChange = (event: React.ChangeEvent) => { @@ -155,6 +155,7 @@ useEffect(() => { }; // Function to close the modal + console.log(filteredInstructors) const handleClose = () => navigate(location.state?.from ? location.state.from : "/courses"); // Render the CourseEditor modal return ( @@ -209,14 +210,24 @@ useEffect(() => { onChange={handleInstitutionChange} // Add onChange to handle institution selection /> Instructors - } - /> + controlId="course-instructor" + name="instructor_id" + disabled={mode === "update" || auth.user.role !== ROLE.SUPER_ADMIN.valueOf()} + options={ + mode === "update" && courseData?.instructor_id && auth.user.role == ROLE.SUPER_ADMIN.valueOf() + ? [ + { + label: users?.data.find((user: any) => String(user.id) === String(courseData.instructor_id))?.name, + value: String(courseData.instructor_id) + }, + ...filteredInstructors + ] + : filteredInstructors + } + inputGroupPrepend={ + Instructors + } +/> Date: Tue, 3 Dec 2024 21:41:48 -0500 Subject: [PATCH 10/10] Finalized Comments --- src/pages/Courses/CourseColumns.tsx | 5 ----- src/pages/Courses/CourseCopy.tsx | 5 ----- src/pages/Courses/CourseDelete.tsx | 7 +------ 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/pages/Courses/CourseColumns.tsx b/src/pages/Courses/CourseColumns.tsx index 8f75ca79..4aeb3393 100644 --- a/src/pages/Courses/CourseColumns.tsx +++ b/src/pages/Courses/CourseColumns.tsx @@ -2,11 +2,6 @@ import { createColumnHelper, Row } from "@tanstack/react-table"; import { Button, Tooltip, OverlayTrigger, Badge } from "react-bootstrap"; import { ICourseResponse as ICourse } from "../../utils/interfaces"; -/** - * Author: Suraj Raghu Kumar on October 27, 2023 - Author: Yuktasree Muppala on October 27, 2023 - Author: Harvardhan Patil on October 27, 2023 - */ type Fn = (row: Row) => void; diff --git a/src/pages/Courses/CourseCopy.tsx b/src/pages/Courses/CourseCopy.tsx index 2e4ca1c1..ae250e03 100644 --- a/src/pages/Courses/CourseCopy.tsx +++ b/src/pages/Courses/CourseCopy.tsx @@ -6,11 +6,6 @@ import { HttpMethod } from "utils/httpMethods"; import useAPI from "../../hooks/useAPI"; import { ICourseResponse as ICourse } from "../../utils/interfaces"; -/** - * @author Suraj Raghu Kumar, on Oct, 2024 - * @author Yuktasree Muppala on Oct, 2024 - * @author Harvardhan Patil on Oct, 2024 - */ // CopyCourse Component: Modal for copying a course. diff --git a/src/pages/Courses/CourseDelete.tsx b/src/pages/Courses/CourseDelete.tsx index ddb6ea04..be3023e7 100644 --- a/src/pages/Courses/CourseDelete.tsx +++ b/src/pages/Courses/CourseDelete.tsx @@ -6,11 +6,6 @@ import { HttpMethod } from "utils/httpMethods"; import useAPI from "../../hooks/useAPI"; import { ICourseResponse as ICourse } from "../../utils/interfaces"; -/** - * @author Suraj Raghu Kumar, on Oct, 2024 - * @author Yuktasree Muppala on Oct, 2024 - * @author Harvardhan Patil on Oct, 2024 - */ // DeleteCourse Component: Modal for deleting a course @@ -20,7 +15,7 @@ interface IDeleteCourse { } const DeleteCourse: React.FC = ({ courseData, onClose }) => { - // State and hook declarations + const { data: deletedCourse, error: courseError, sendRequest: DeleteCourse } = useAPI(); const [show, setShow] = useState(true); const dispatch = useDispatch();