From 147c947ea9c9b1b1a58d29c251d799f5a43bd3fd Mon Sep 17 00:00:00 2001 From: MakarandPundlik <65530539+MakarandPundlik@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:29:24 -0500 Subject: [PATCH 001/100] Updated README.md added contributors name --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index b87cb004..97749e4d 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,8 @@ You don’t have to ever use `eject`. The curated feature set is suitable for sm You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). To learn React, check out the [React documentation](https://reactjs.org/). + +## Contributers +Makarand Pundlik
+Anurag Gorkar
+Rutvik Kulkarni
From 159ca63a61966d63030180e47aaa45c942d91398 Mon Sep 17 00:00:00 2001 From: MakarandPundlik <65530539+MakarandPundlik@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:37:33 -0500 Subject: [PATCH 002/100] Added npm install command --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 97749e4d..542d9421 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ This project was bootstrapped with [Create React App](https://github.com/faceboo In the project directory, you can run: +### `npm install` +Installs the dependencies required + ### `npm start` Runs the app in the development mode.\ From 52d4f29645a4bb1e9dcc6eb45df6f89d5273f048 Mon Sep 17 00:00:00 2001 From: Rutvik Vishwas Kulkarni Date: Sun, 24 Nov 2024 17:50:33 -0500 Subject: [PATCH 003/100] UI enhancements to the TA and Course page --- src/components/ColumnButton.tsx | 57 +++++++++++++++++++++++++++++ src/pages/Courses/CourseColumns.tsx | 43 +++++++++++++++------- src/pages/TA/TA.tsx | 48 +++++++++++++++--------- src/pages/TA/TAColumns.tsx | 10 +++-- 4 files changed, 122 insertions(+), 36 deletions(-) create mode 100644 src/components/ColumnButton.tsx diff --git a/src/components/ColumnButton.tsx b/src/components/ColumnButton.tsx new file mode 100644 index 00000000..25eea86f --- /dev/null +++ b/src/components/ColumnButton.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { Button, OverlayTrigger, Tooltip } from "react-bootstrap"; + +/** + * @author Rutvik Kulkarni on Nov, 2024 + */ + +interface ColumnButtonProps { + id: string; + label?: string; + tooltip?: string; + variant: string; + size?: "sm" | "lg"; // Matches React-Bootstrap Button prop + className?: string; + onClick: () => void; + icon: React.ReactNode; + } + + const ColumnButton: React.FC = (props) => { + const { + id, + label, + tooltip, + variant, + size, + className, + onClick, + icon, + } = props; + + const displayButton = ( + + ); + + if (tooltip) { + return ( + {tooltip}} + > + {displayButton} + + ); + } + + return displayButton; + }; + + export default ColumnButton; \ No newline at end of file diff --git a/src/pages/Courses/CourseColumns.tsx b/src/pages/Courses/CourseColumns.tsx index 25002d6f..59665c3c 100644 --- a/src/pages/Courses/CourseColumns.tsx +++ b/src/pages/Courses/CourseColumns.tsx @@ -3,6 +3,8 @@ import { Button } from "react-bootstrap"; import { BsPencilFill, BsPersonXFill } from "react-icons/bs"; import { MdContentCopy, MdDelete } from "react-icons/md"; import { ICourseResponse as ICourse } from "../../utils/interfaces"; +import ColumnButton from "../../components/ColumnButton"; + /** * @author Atharva Thorve, on December, 2023 @@ -53,28 +55,41 @@ export const courseColumns = (handleEdit: Fn, handleDelete: Fn, handleTA: Fn, ha header: "Actions", cell: ({ row }) => ( <> - - - - + tooltip="Copy course details" + icon={} + /> ), }), diff --git a/src/pages/TA/TA.tsx b/src/pages/TA/TA.tsx index 87a1b334..f15ffcb5 100644 --- a/src/pages/TA/TA.tsx +++ b/src/pages/TA/TA.tsx @@ -12,6 +12,7 @@ import { alertActions } from "store/slices/alertSlice"; import { RootState } from "../../store/store"; import { ITAResponse, ROLE } from "../../utils/interfaces"; import { TAColumns as TA_COLUMNS } from "./TAColumns"; +import ColumnButton from "../../components/ColumnButton"; import DeleteTA from "./TADelete"; /** @@ -85,26 +86,37 @@ const TAs = () => {
- - + + navigate("new")} + tooltip="Add TA to this course" + icon={} + /> - {showDeleteConfirmation.visible && ( - - )} - - - + {tableData.length === 0 ? ( + + +

No TAs are assigned for this course.

+ + + ) : ( + +
+ + )} diff --git a/src/pages/TA/TAColumns.tsx b/src/pages/TA/TAColumns.tsx index c4545b5f..03b7b88f 100644 --- a/src/pages/TA/TAColumns.tsx +++ b/src/pages/TA/TAColumns.tsx @@ -3,6 +3,7 @@ import { createColumnHelper, Row } from "@tanstack/react-table"; import { Button } from "react-bootstrap"; import { BsPersonXFill } from "react-icons/bs"; import { ITAResponse as ITA } from "../../utils/interfaces"; +import ColumnButton from "../../components/ColumnButton"; /** * @author Atharva Thorve, on December, 2023 @@ -38,14 +39,15 @@ export const TAColumns = (handleDelete: Fn) => [ header: "Actions", cell: ({ row }) => ( <> - + tooltip="Delete TA" + icon={} + /> ), }), From 65421d9775a8c44b8af54d12afd1afe60802bcd5 Mon Sep 17 00:00:00 2001 From: MakarandPundlik Date: Thu, 28 Nov 2024 17:47:51 -0500 Subject: [PATCH 004/100] resolved a bug by adding select TA to the final array and removed it from the interface --- src/pages/TA/TAUtil.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/TA/TAUtil.ts b/src/pages/TA/TAUtil.ts index d49d99fa..c7a34f91 100644 --- a/src/pages/TA/TAUtil.ts +++ b/src/pages/TA/TAUtil.ts @@ -17,7 +17,7 @@ export interface ITAFormValues { } export const transformTAResponse = (taList: string) => { - let taData: IFormOption[] = [{ label: "Select a TA", value: "" }]; + let taData: IFormOption[] = []; let tas: ITA[] = JSON.parse(taList); tas.forEach((ta) => taData.push({ label: ta.name, value: ta.id! })); return taData; @@ -37,7 +37,7 @@ export async function loadTAs({ params }: any) { const taRoleUsersResponse = await axiosClient.get(`/users/role/Teaching Assistant`, { transformResponse: transformTAResponse }); - const taUsers = taRoleUsersResponse.data; - + let taUsers = taRoleUsersResponse.data; + taUsers = [{label: "Select a TA", value: ""},...taUsers]; return { taUsers }; } From 86265917854455dfd92d585ce10f68bcbd807025 Mon Sep 17 00:00:00 2001 From: MakarandPundlik Date: Sat, 30 Nov 2024 12:10:52 -0500 Subject: [PATCH 005/100] added unit test cases for columns --- src/pages/TA/TAColumns.test.tsx | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/pages/TA/TAColumns.test.tsx diff --git a/src/pages/TA/TAColumns.test.tsx b/src/pages/TA/TAColumns.test.tsx new file mode 100644 index 00000000..eb411a12 --- /dev/null +++ b/src/pages/TA/TAColumns.test.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Row } from "@tanstack/react-table"; +import { TAColumns } from "./TAColumns"; + +// Mock the ColumnButton component +jest.mock("../../components/ColumnButton", () => ({ id, ...props }: any) => ( + +)); + +describe("TAColumns", () => { + const mockHandleDelete = jest.fn(); + const mockRow: Partial> = { original: { id: "123", name: "Test TA" } }; + + test("should define all required columns", () => { + const columns = TAColumns(mockHandleDelete); + expect(columns).toHaveLength(5); + + // Check each column's header + expect(columns[0].header).toBe("Id"); + expect(columns[1].header).toBe("TA Name"); + expect(columns[2].header).toBe("Full Name"); + expect(columns[3].header).toBe("Email"); + expect(columns[4].header).toBe("Actions"); + }); + + test("should correctly render the actions column", () => { + const actionsColumn = TAColumns(mockHandleDelete).find((col) => col.id === "actions"); + expect(actionsColumn).toBeDefined(); + const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; + + render(} />); + const deleteButton = screen.getByTestId("delete-ta"); + expect(deleteButton).toBeTruthy(); + // expect(deleteButton).toContain("Delete TA"); + // expect(deleteButton).toHaveAttribute("tooltip", "Delete TA"); + }); + + test("should call handleDelete when delete button is clicked", async () => { + const actionsColumn = TAColumns(mockHandleDelete).find((col) => col.id === "actions"); + const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; + + render(} />); + const deleteButton = screen.getByTestId("delete-ta"); + + await userEvent.click(deleteButton); + expect(mockHandleDelete).toHaveBeenCalledTimes(1); + expect(mockHandleDelete).toHaveBeenCalledWith(mockRow); + }); +}); From 104268ac0c5a1a20b63c8e345bd404f604bb4818 Mon Sep 17 00:00:00 2001 From: MakarandPundlik Date: Sat, 30 Nov 2024 12:14:31 -0500 Subject: [PATCH 006/100] added unit test cases for coursecolumns --- src/pages/Courses/CourseColumns.test.tsx | 101 +++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/pages/Courses/CourseColumns.test.tsx diff --git a/src/pages/Courses/CourseColumns.test.tsx b/src/pages/Courses/CourseColumns.test.tsx new file mode 100644 index 00000000..03436a51 --- /dev/null +++ b/src/pages/Courses/CourseColumns.test.tsx @@ -0,0 +1,101 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Row } from "@tanstack/react-table"; +import { courseColumns } from "./CourseColumns"; +// import ColumnButton from "../../components/ColumnButton"; + +// Mock the ColumnButton component +jest.mock("../../components/ColumnButton", () => ({ id, ...props }: any) => ( + +)); + +describe("courseColumns", () => { + const mockHandleEdit = jest.fn(); + const mockHandleDelete = jest.fn(); + const mockHandleTA = jest.fn(); + const mockHandleCopy = jest.fn(); + const mockRow: Partial> = { + original: { id: "123", name: "Test Course", institution: { name: "Test Institution" } }, + }; + + test("should define all required columns", () => { + const columns = courseColumns(mockHandleEdit, mockHandleDelete, mockHandleTA, mockHandleCopy); + expect(columns).toHaveLength(5); + + // Check each column's header + expect(columns[0].header).toBe("Name"); + expect(columns[1].header).toBe("Institution"); + expect(columns[2].header).toBe("Creation Date"); + expect(columns[3].header).toBe("Updated Date"); + expect(columns[4].header).toBe("Actions"); + }); + + test("should call handleEdit when edit button is clicked", async () => { + const actionsColumn = courseColumns( + mockHandleEdit, + mockHandleDelete, + mockHandleTA, + mockHandleCopy + ).find((col) => col.id === "actions"); + const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; + + render(} />); + const editButton = screen.getByTestId("edit"); + + userEvent.click(editButton); + expect(mockHandleEdit).toHaveBeenCalledTimes(1); + expect(mockHandleEdit).toHaveBeenCalledWith(mockRow); + }); + + test("should call handleDelete when delete button is clicked", async () => { + const actionsColumn = courseColumns( + mockHandleEdit, + mockHandleDelete, + mockHandleTA, + mockHandleCopy + ).find((col) => col.id === "actions"); + const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; + + render(} />); + const deleteButton = screen.getByTestId("delete"); + + userEvent.click(deleteButton); + expect(mockHandleDelete).toHaveBeenCalledTimes(1); + expect(mockHandleDelete).toHaveBeenCalledWith(mockRow); + }); + + test("should call handleTA when assign TA button is clicked", async () => { + const actionsColumn = courseColumns( + mockHandleEdit, + mockHandleDelete, + mockHandleTA, + mockHandleCopy + ).find((col) => col.id === "actions"); + const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; + + render(} />); + const assignTAButton = screen.getByTestId("assign-ta"); + + userEvent.click(assignTAButton); + expect(mockHandleTA).toHaveBeenCalledTimes(1); + expect(mockHandleTA).toHaveBeenCalledWith(mockRow); + }); + + test("should call handleCopy when copy button is clicked", async () => { + const actionsColumn = courseColumns( + mockHandleEdit, + mockHandleDelete, + mockHandleTA, + mockHandleCopy + ).find((col) => col.id === "actions"); + const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; + + render(} />); + const copyButton = screen.getByTestId("copy"); + + userEvent.click(copyButton); + expect(mockHandleCopy).toHaveBeenCalledTimes(1); + expect(mockHandleCopy).toHaveBeenCalledWith(mockRow); + }); +}); From d3da1da8cd83952613b8b3372bc379259d4e56b0 Mon Sep 17 00:00:00 2001 From: MakarandPundlik Date: Sat, 30 Nov 2024 12:19:30 -0500 Subject: [PATCH 007/100] added test cases for column button --- src/components/ColumnButton.test.tsx | 57 ++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/components/ColumnButton.test.tsx diff --git a/src/components/ColumnButton.test.tsx b/src/components/ColumnButton.test.tsx new file mode 100644 index 00000000..20e22b42 --- /dev/null +++ b/src/components/ColumnButton.test.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import ColumnButton from "./ColumnButton"; + +// Mock React-Bootstrap components +jest.mock("react-bootstrap", () => ({ + Button: ({ children, ...props }: any) => , + Tooltip: ({ id, children }: any) =>
{children}
, + OverlayTrigger: ({ children, overlay }: any) => ( +
+ {overlay} + {children} +
+ ), +})); + +describe("ColumnButton", () => { + const mockOnClick = jest.fn(); + + const baseProps = { + id: "test-button", + label: "Test Button", + tooltip: "This is a test tooltip", + variant: "primary", + size: "sm" as const, + className: "custom-class", + onClick: mockOnClick, + icon: Icon, + }; + + test("should call onClick when the button is clicked", async () => { + render(); + const button = screen.getByRole("button", { name: "Test Button" }); + + userEvent.click(button); + expect(mockOnClick).toHaveBeenCalledTimes(1); + }); + + test("should render a tooltip when tooltip is provided", () => { + render(); + const tooltip = screen.getByText("This is a test tooltip"); + + // Vanilla assertion to check if tooltip is rendered + expect(tooltip).not.toBeNull(); + const button = screen.getByRole("button", { name: "Test Button" }); + expect(button).not.toBeNull(); // Ensure the button is still present + }); + + test("should not render a tooltip when tooltip is not provided", () => { + render(); + const tooltip = screen.queryByText("This is a test tooltip"); + + // Vanilla assertion to check if tooltip is not rendered + expect(tooltip).toBeNull(); + }); +}); From bd54b4e223cee11357884ff1f8dee2961a19f167 Mon Sep 17 00:00:00 2001 From: Anurag Dilip Gorkar Date: Sat, 30 Nov 2024 15:04:08 -0500 Subject: [PATCH 008/100] Added select feature to Add TA drop down --- package-lock.json | 274 ++++++++++++++++++++++++++++- package.json | 5 +- src/App.test.tsx | 9 - src/hooks/useAPI.ts | 2 +- src/pages/Authentication/Login.tsx | 2 +- src/pages/TA/TAEditor.tsx | 178 ++++++++++++------- src/utils/axios_client.ts | 2 +- 7 files changed, 385 insertions(+), 87 deletions(-) delete mode 100644 src/App.test.tsx diff --git a/package-lock.json b/package-lock.json index 3ef4561e..aa257d7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,8 +24,8 @@ "@types/react-router-dom": "^5.3.3", "axios": "^1.4.0", "bootstrap": "^5.3.3", - "chart.js": "^3.7.0", - "formik": "^2.2.9", + "chart.js": "^4.1.1", + "formik": "^2.4.6", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", "react": "^18.2.0", @@ -38,7 +38,8 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", - "recharts": "^2.12.3", + "react-select": "^5.8.3", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", @@ -2393,6 +2394,147 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.5.tgz", + "integrity": "sha512-Z3xbtJ+UcK76eWkagZ1onvn/wAVb1GOMuR15s30Fm2wrMgC7jzpnO2JZXr4eujTTqoQFUrZIw/rT0c6Zzjca1g==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.5.tgz", + "integrity": "sha512-6zeCUxUH+EPF1s+YF/2hPVODeV/7V07YU5x+2tfuRL8MdW6rv5vb2+CBEGTGwBdux0OIERcOS+RzxeK80k2DsQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", + "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2486,6 +2628,31 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", + "license": "MIT" + }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", @@ -3045,6 +3212,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -5760,9 +5933,16 @@ } }, "node_modules/chart.js": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz", - "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg==" + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz", + "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } }, "node_modules/check-types": { "version": "11.2.3", @@ -8363,6 +8543,12 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -8563,15 +8749,16 @@ } }, "node_modules/formik": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.5.tgz", - "integrity": "sha512-Gxlht0TD3vVdzMDHwkiNZqJ7Mvg77xQNfmBRrNtvzcHZs72TJppSTDKHpImCMJZwcWPBJ8jSQQ95GJzXFf1nAQ==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz", + "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==", "funding": [ { "type": "individual", "url": "https://opencollective.com/formik" } ], + "license": "Apache-2.0", "dependencies": { "@types/hoist-non-react-statics": "^3.3.1", "deepmerge": "^2.1.1", @@ -11372,6 +11559,12 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -14168,6 +14361,27 @@ } } }, + "node_modules/react-select": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.3.tgz", + "integrity": "sha512-lVswnIq8/iTj1db7XCG74M/3fbGB6ZaluCzvwPGT5ZOjCdL/k0CLWhEK0vCBLuU5bHTEf6Gj8jtSvi+3v+tO1w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-smooth": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", @@ -15628,6 +15842,12 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -16529,6 +16749,20 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -16603,6 +16837,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/package.json b/package.json index a1ed9d64..668f6cb0 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,7 @@ "axios": "^1.4.0", "bootstrap": "^5.3.3", "chart.js": "^4.1.1", - "recharts": "^2.0.0", - "formik": "^2.2.9", + "formik": "^2.4.6", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", "react": "^18.2.0", @@ -34,6 +33,8 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", + "react-select": "^5.8.3", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", diff --git a/src/App.test.tsx b/src/App.test.tsx deleted file mode 100644 index 2a68616d..00000000 --- a/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/hooks/useAPI.ts b/src/hooks/useAPI.ts index 47ba7ee5..6db9fa61 100644 --- a/src/hooks/useAPI.ts +++ b/src/hooks/useAPI.ts @@ -6,7 +6,7 @@ import { getAuthToken } from "../utils/auth"; * @author Ankur Mundra on April, 2023 */ -axios.defaults.baseURL = "http://localhost:3002/api/v1"; +axios.defaults.baseURL = "http://localhost:3000/api/v1"; axios.defaults.headers.common["Accept"] = "application/json"; axios.defaults.headers.post["Content-Type"] = "application/json"; axios.defaults.headers.put["Content-Type"] = "application/json"; diff --git a/src/pages/Authentication/Login.tsx b/src/pages/Authentication/Login.tsx index 2051b297..d7bf781d 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://localhost:3002/login", values) + .post("http://localhost:3000/login", values) .then((response) => { const payload = setAuthToken(response.data.token); diff --git a/src/pages/TA/TAEditor.tsx b/src/pages/TA/TAEditor.tsx index 6d742c05..86c97fa7 100644 --- a/src/pages/TA/TAEditor.tsx +++ b/src/pages/TA/TAEditor.tsx @@ -1,8 +1,7 @@ -// Importing necessary interfaces and modules -import FormSelect from "components/Form/FormSelect"; +import React, { useEffect, useState } from "react"; +import Select from 'react-select'; import { Form, Formik, FormikHelpers } from "formik"; import useAPI from "hooks/useAPI"; -import React, { useEffect } from "react"; import { Button, InputGroup, Modal } from "react-bootstrap"; import { useDispatch } from "react-redux"; import { useLoaderData, useLocation, useNavigate, useParams } from "react-router-dom"; @@ -12,10 +11,12 @@ import * as Yup from "yup"; import { IEditor } from "../../utils/interfaces"; import { ITAFormValues, transformTARequest } from "./TAUtil"; -/** - * @author Atharva Thorve, on December, 2023 - * @author Divit Kalathil, on December, 2023 - */ +// Type definition for user options +type UserOption = { + label: string; + value: string | number; + role?: string; +}; const initialValues: ITAFormValues = { name: "", @@ -29,7 +30,6 @@ const TAEditor: React.FC = ({ mode }) => { const { data: TAResponse, error: TAError, sendRequest } = useAPI(); const TAData = { ...initialValues }; - // Load data from the server const { taUsers }: any = useLoaderData(); const dispatch = useDispatch(); const navigate = useNavigate(); @@ -37,9 +37,9 @@ const TAEditor: React.FC = ({ mode }) => { const params = useParams(); const { courseId } = params; - // logged-in TA is the parent of the TA being created and the institution is the same as the parent's + const [showConfirmModal, setShowConfirmModal] = useState(false); + const [selectedUser, setSelectedUser] = useState({ label: "", value: "" }); - // Close the modal if the TA is updated successfully and navigate to the TAs page useEffect(() => { if (TAResponse && TAResponse.status >= 200 && TAResponse.status < 300) { dispatch( @@ -48,80 +48,130 @@ const TAEditor: React.FC = ({ mode }) => { message: `TA ${TAData.name} ${mode}d successfully!`, }) ); - navigate(location.state?.from ? location.state.from : "/TAs"); + navigate(location.state?.from ? location.state.from : `/courses/${courseId}/tas`); } - }, [dispatch, mode, navigate, TAData.name, TAResponse, location.state?.from]); + }, [dispatch, mode, navigate, TAData.name, TAResponse, location.state?.from, showConfirmModal]); - // Show the error message if the TA is not updated successfully useEffect(() => { TAError && dispatch(alertActions.showAlert({ variant: "danger", message: TAError })); }, [TAError, dispatch]); const onSubmit = (values: ITAFormValues, submitProps: FormikHelpers) => { + const selectedUserData = taUsers.find((user: UserOption) => + parseInt(String(user.value)) === parseInt(String(values.name)) + ); + + if (selectedUserData?.role === 'student') { + // If selected user is a student, show confirmation modal + console.log("Student role detected...", selectedUserData); + setSelectedUser(selectedUserData); + setShowConfirmModal(true); + } else { + // If TA or other role, directly submit + submitTA(values); + } + submitProps.setSubmitting(false); + }; + + const submitTA = (values: ITAFormValues) => { let method: HttpMethod = HttpMethod.GET; - // ToDo: Need to create API in the backend for this call. - // Note: The current API needs the TA id to create a new TA which is incorrect and needs to be fixed. - // Currently we send the username of the user we want to add as the TA for the course. let url: string = `/courses/${courseId}/add_ta/${values.name}`; - // to be used to display message when TA is created sendRequest({ url: url, method: method, data: {}, transformRequest: transformTARequest, }); - submitProps.setSubmitting(false); + }; + + const handleConfirmAddStudent = () => { + // Submit TA addition if confirmed + submitTA({ name: String(selectedUser.value) }); + setShowConfirmModal(false); }; const handleClose = () => navigate(location.state?.from ? location.state.from : `/courses/${courseId}/tas`); - //Validation of TA Entry + return ( - - - Add TA - - - {TAError &&

{TAError}

} - - {(formik) => { - return ( -
- TA - } - /> - - - - - - - ); - }} -
-
-
+ <> + + + Add TA + + + {TAError &&

{TAError}

} + + {(formik) => { + return ( +
+
+ +
+ + ); +}; + +export default SubmissionList; From 6e05f210a4d61825e12c0a2a6aeb4b0ba575bda3 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 17:24:31 -0400 Subject: [PATCH 017/100] create submission view component --- src/pages/Submissions/SubmissionsView.tsx | 97 +++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/pages/Submissions/SubmissionsView.tsx diff --git a/src/pages/Submissions/SubmissionsView.tsx b/src/pages/Submissions/SubmissionsView.tsx new file mode 100644 index 00000000..39f8cf2d --- /dev/null +++ b/src/pages/Submissions/SubmissionsView.tsx @@ -0,0 +1,97 @@ +import { useEffect, useState } from "react"; +import { Col, Container, Form, Row } from "react-bootstrap"; +import SubmissionList from "./SubmissionTable/SubmissionList"; + +const SubmissionView = () => { + const [submissions, setSubmissions] = useState([]); + const [filteredSubmissions, setFilteredSubmissions] = useState([]); + const [assignmentFilter, setAssignmentFilter] = useState(""); + + // Dummy assignments for filtering + const assignments = ["Assignment 1", "Assignment 2", "Assignment 3"]; + + useEffect(() => { + // Simulating data fetching + const fetchSubmissions = async () => { + const data = [ + { + id: 1, + teamName: "Anonymized_Team_38121", + assignment: "Assignment 1", + members: [ + { name: "Student 10566", id: 10566 }, + { name: "Student 10559", id: 10559 }, + { name: "Student 10359", id: 10359 }, + ], + links: [ + { url: "https://github.com/example/repo", displayName: "GitHub Repository" }, + { url: "http://google.com", displayName: "Submission Link" }, + ], + fileInfo: [ + { name: "README.md", size: "14.9 KB", dateModified: "2024-10-03 23:36:57" }, + ], + }, + { + id: 2, + teamName: "Anonymized_Team_38122", + assignment: "Assignment 2", + members: [ + { name: "Student 10593", id: 10593 }, + { name: "Student 10623", id: 10623 }, + ], + links: [ + { url: "https://github.com/example/repo2", displayName: "GitHub Repository" }, + ], + fileInfo: [ + { name: "README.md", size: "11.7 KB", dateModified: "2024-10-01 12:15:00" }, + ], + }, + ]; + + setSubmissions(data); + setFilteredSubmissions(data); + }; + + fetchSubmissions(); + }, []); + + const handleGradeClick = (id: number) => { + console.log(`Assign Grade clicked for submission ID ${id}`); + }; + + const handleAssignmentChange = (e: React.ChangeEvent) => { + const selectedAssignment = e.target.value; + setAssignmentFilter(selectedAssignment); + if (selectedAssignment) { + setFilteredSubmissions(submissions.filter(sub => sub.assignment === selectedAssignment)); + } else { + setFilteredSubmissions(submissions); + } + }; + + return ( + + + +

Submissions

+
+ + Filter by Assignment + handleAssignmentChange(e as any)}> + + {assignments.map((assignment, index) => ( + + ))} + + + + + + + + + + ); +}; + +export default SubmissionView; From 0d284963fa07febbb6b0f21b0601a7158fbd1790 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 17:30:02 -0400 Subject: [PATCH 018/100] add route for submission view --- src/App.tsx | 64 ++++++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 27736ba3..9230c739 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,45 +1,45 @@ -import React from "react"; +import RootLayout from "layout/Root"; +import { loadAssignment } from "pages/Assignments/AssignmentUtil"; +import AssignReviewer from "pages/Assignments/AssignReviewer"; +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 Courses from "pages/Courses/Course"; +import CourseEditor from "pages/Courses/CourseEditor"; +import { loadCourseInstructorDataAndInstitutions } from "pages/Courses/CourseUtil"; +import Questionnaire from "pages/EditQuestionnaire/Questionnaire"; +import Home from "pages/Home"; +import Participants from "pages/Participants/Participant"; +import ParticipantEditor from "pages/Participants/ParticipantEditor"; +import { loadParticipantDataRolesAndInstitutions } from "pages/Participants/participantUtil"; +import EditProfile from "pages/Profile/Edit"; +import Reviews from "pages/Reviews/reviews"; +import SubmissionsView from "pages/Submissions/SubmissionsView"; +import TA from "pages/TA/TA"; +import TAEditor from "pages/TA/TAEditor"; +import { loadTAs } from "pages/TA/TAUtil"; import { createBrowserRouter, Navigate, RouterProvider } from "react-router-dom"; import AdministratorLayout from "./layout/Administrator"; import ManageUserTypes, { loader as loadUsers } from "./pages/Administrator/ManageUserTypes"; +import Assignment from "./pages/Assignments/Assignment"; +import AssignmentEditor from "./pages/Assignments/AssignmentEditor"; import Login from "./pages/Authentication/Login"; import Logout from "./pages/Authentication/Logout"; +import Email_the_author from "./pages/Email_the_author/email_the_author"; 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 { loadParticipantDataRolesAndInstitutions } from "pages/Participants/participantUtil"; -import RootLayout from "layout/Root"; -import UserEditor from "./pages/Users/UserEditor"; import Users from "./pages/Users/User"; +import UserEditor from "./pages/Users/UserEditor"; 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 ReviewTable from "./pages/ViewTeamGrades/ReviewTable"; -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 ErrorPage from "./router/ErrorPage"; +import NotFound from "./router/NotFound"; +import ProtectedRoute from "./router/ProtectedRoute"; +import { ROLE } from "./utils/interfaces"; function App() { const router = createBrowserRouter([ { @@ -122,6 +122,10 @@ function App() { }, ], }, + { + path: "student_tasks", + element: } leastPrivilegeRole={ROLE.TA} />, + }, { path: "student_tasks/participants", element: , From a5eba6313495119bf85e7cb370868bc1121eb854 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 17:30:59 -0400 Subject: [PATCH 019/100] fix styling for container --- src/pages/ViewTeamGrades/grades.scss | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/ViewTeamGrades/grades.scss b/src/pages/ViewTeamGrades/grades.scss index e422e64f..f80b9564 100644 --- a/src/pages/ViewTeamGrades/grades.scss +++ b/src/pages/ViewTeamGrades/grades.scss @@ -231,10 +231,8 @@ .container { display: flex; - justify-content: space-between; - /* Adjust as needed */ - width: 80%; - /* Ensure the container takes up the full width */ + justify-content: space-between; /* Adjust as needed */ + width: 100%; /* Ensure the container takes up the full width */ } From 6b26e8433e4b6f194975e24cb3335176c85c151b Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 17:45:22 -0400 Subject: [PATCH 020/100] test submission view component --- .../Submissions/SubmissionsView.test.tsx | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/pages/Submissions/SubmissionsView.test.tsx diff --git a/src/pages/Submissions/SubmissionsView.test.tsx b/src/pages/Submissions/SubmissionsView.test.tsx new file mode 100644 index 00000000..d0df159b --- /dev/null +++ b/src/pages/Submissions/SubmissionsView.test.tsx @@ -0,0 +1,54 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import SubmissionView from './SubmissionsView'; + +describe('SubmissionsView', () => { + it('renders the title and filter', () => { + render( + + + + ); + + const title = screen.getByText('Submissions'); + const filter = screen.getByLabelText('Filter by Assignment'); + + expect(title).toBeTruthy(); + expect(filter).toBeTruthy(); + }); + + it('filters submissions based on selected assignment', async () => { + render( + + + + ); + + // Select an assignment to filter + const select = screen.getByLabelText('Filter by Assignment'); + fireEvent.change(select, { target: { value: 'Assignment 1' } }); + + // Check if the filtered submission is displayed + expect(await screen.findByText('Anonymized_Team_38121')).toBeTruthy(); + expect(screen.queryByText('Anonymized_Team_38122')).toBeFalsy(); + }); + + it('shows all submissions when no filter is applied', async () => { + render( + + + + ); + + // Select an assignment to filter + const select = screen.getByLabelText('Filter by Assignment'); + fireEvent.change(select, { target: { value: 'Assignment 1' } }); + + // Reset filter + fireEvent.change(select, { target: { value: '' } }); + + // Check if all submissions are displayed + expect(await screen.findByText('Anonymized_Team_38121')).toBeTruthy(); + expect(await screen.findByText('Anonymized_Team_38122')).toBeTruthy(); + }); +}); From 0a0238da99da85302a729e748ee09ad7a653fdf5 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 17:45:34 -0400 Subject: [PATCH 021/100] test submission list component --- .../SubmissionTable/SubmissionList.test.tsx | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/pages/Submissions/SubmissionTable/SubmissionList.test.tsx diff --git a/src/pages/Submissions/SubmissionTable/SubmissionList.test.tsx b/src/pages/Submissions/SubmissionTable/SubmissionList.test.tsx new file mode 100644 index 00000000..5f6ab171 --- /dev/null +++ b/src/pages/Submissions/SubmissionTable/SubmissionList.test.tsx @@ -0,0 +1,68 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import SubmissionList from './SubmissionList'; + +const mockSubmissions = [ + { + id: 1, + teamName: 'Team B', + assignment: 'Assignment 1', + members: [{ name: 'Student 1', id: 1 }], + links: [], + fileInfo: [], + }, + { + id: 2, + teamName: 'Team A', + assignment: 'Assignment 1', + members: [{ name: 'Student 2', id: 2 }], + links: [], + fileInfo: [], + }, +]; + +const mockOnGradeClick = jest.fn(); + +describe('SubmissionList', () => { + it('renders submission entries correctly', () => { + render( + + + + ); + + // Check if submission entry is rendered + expect(screen.getByText('Team B')).toBeTruthy(); + expect(screen.getByText('Team A')).toBeTruthy(); + }); + + it('sorts the submissions by team name', () => { + render( + + + + ); + + // Click the team name header to sort ascending + const teamNameHeader = screen.getByText('Team Name'); + fireEvent.click(teamNameHeader); + + // Get the rows that contain submission entries + const rows = screen.getAllByRole('row'); + + // Check the order of the first two submission rows (excluding the header) + expect(rows[1].innerHTML).toContain('Team A'); + expect(rows[2].innerHTML).toContain('Team B'); + + // Click again to sort descending + fireEvent.click(teamNameHeader); + + // Get the rows again after sorting + const sortedRows = screen.getAllByRole('row'); + + // Check the order of the first two submission rows (excluding the header) + expect(sortedRows[1].innerHTML).toContain('Team B'); + expect(sortedRows[2].innerHTML).toContain('Team A'); + }); + +}); From 34a9e0a15296badfc49346ff1fb7ba6fb669b309 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 17:45:48 -0400 Subject: [PATCH 022/100] test submission entry component --- .../SubmissionTable/SubmissionEntry.test.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/pages/Submissions/SubmissionTable/SubmissionEntry.test.tsx diff --git a/src/pages/Submissions/SubmissionTable/SubmissionEntry.test.tsx b/src/pages/Submissions/SubmissionTable/SubmissionEntry.test.tsx new file mode 100644 index 00000000..41e2790b --- /dev/null +++ b/src/pages/Submissions/SubmissionTable/SubmissionEntry.test.tsx @@ -0,0 +1,43 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import SubmissionList from './SubmissionList'; + +const mockSubmissions = [ + { + id: 1, + teamName: 'Anonymized_Team_38121', + assignment: 'Assignment 1', + members: [{ name: 'Student 1', id: 1 }], + links: [], + fileInfo: [], + }, +]; + +const mockOnGradeClick = jest.fn(); + +describe('SubmissionEntry', () => { + it('displays the correct team name', () => { + render( + + + + ); + + // Check if team name is rendered correctly + expect(screen.getByText('Anonymized_Team_38121')).toBeTruthy(); + }); + + it('calls onGradeClick when the grade button is clicked', () => { + render( + + + + ); + + // Simulate the button click + const button = screen.getByRole('button', { name: /Assign Grade/i }); + fireEvent.click(button); + + expect(mockOnGradeClick).toHaveBeenCalledWith(mockSubmissions[0].id); + }); +}); From 6c1d8a5ef7fa634dedeaa8633618b92d3bb59f9f Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 18:00:18 -0400 Subject: [PATCH 023/100] move existing individual submission view to submissions directory --- src/App.tsx | 4 ++-- .../SubmissionView.tsx} | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/pages/{Assignments/ViewSubmissions.tsx => Submissions/SubmissionView.tsx} (94%) diff --git a/src/App.tsx b/src/App.tsx index 9230c739..7e07e27a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,6 @@ 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 Courses from "pages/Courses/Course"; import CourseEditor from "pages/Courses/CourseEditor"; import { loadCourseInstructorDataAndInstitutions } from "pages/Courses/CourseUtil"; @@ -17,6 +16,7 @@ import { loadParticipantDataRolesAndInstitutions } from "pages/Participants/part import EditProfile from "pages/Profile/Edit"; import Reviews from "pages/Reviews/reviews"; import SubmissionsView from "pages/Submissions/SubmissionsView"; +import SubmissionView from "pages/Submissions/SubmissionView"; import TA from "pages/TA/TA"; import TAEditor from "pages/TA/TAEditor"; import { loadTAs } from "pages/TA/TAUtil"; @@ -72,7 +72,7 @@ function App() { }, { path: "assignments/edit/:id/viewsubmissions", - element: , + element: , loader: loadAssignment, }, { diff --git a/src/pages/Assignments/ViewSubmissions.tsx b/src/pages/Submissions/SubmissionView.tsx similarity index 94% rename from src/pages/Assignments/ViewSubmissions.tsx rename to src/pages/Submissions/SubmissionView.tsx index d9fd69b1..48b31f77 100644 --- a/src/pages/Assignments/ViewSubmissions.tsx +++ b/src/pages/Submissions/SubmissionView.tsx @@ -1,8 +1,8 @@ import React, { useMemo } from 'react'; -import { Button, Container, Row, Col } from 'react-bootstrap'; +import { Button, Col, Container, Row } from 'react-bootstrap'; // import { useNavigate } from 'react-router-dom'; -import Table from "components/Table/Table"; import { createColumnHelper } from "@tanstack/react-table"; +import Table from "components/Table/Table"; import { useLoaderData } from 'react-router-dom'; interface ISubmission { @@ -12,7 +12,7 @@ interface ISubmission { const columnHelper = createColumnHelper(); -const ViewSubmissions: React.FC = () => { +const SubmissionView: React.FC = () => { const assignment: any = useLoaderData(); // const navigate = useNavigate(); @@ -81,4 +81,4 @@ const ViewSubmissions: React.FC = () => { ); }; -export default ViewSubmissions; \ No newline at end of file +export default SubmissionView; \ No newline at end of file From ce309c9bcabd6f3ae947ff7c1ec0c2dc0a6081ba Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sat, 19 Oct 2024 13:33:48 -0400 Subject: [PATCH 024/100] update view to include 23 mocked users --- src/pages/Submissions/SubmissionView.tsx | 7 -- src/pages/Submissions/SubmissionsView.tsx | 83 +++++++++++++---------- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/pages/Submissions/SubmissionView.tsx b/src/pages/Submissions/SubmissionView.tsx index 48b31f77..4e8f14cc 100644 --- a/src/pages/Submissions/SubmissionView.tsx +++ b/src/pages/Submissions/SubmissionView.tsx @@ -70,13 +70,6 @@ const SubmissionView: React.FC = () => { /> - {/* - - - - */} ); }; diff --git a/src/pages/Submissions/SubmissionsView.tsx b/src/pages/Submissions/SubmissionsView.tsx index 39f8cf2d..1ed53ba5 100644 --- a/src/pages/Submissions/SubmissionsView.tsx +++ b/src/pages/Submissions/SubmissionsView.tsx @@ -13,40 +13,41 @@ const SubmissionView = () => { useEffect(() => { // Simulating data fetching const fetchSubmissions = async () => { - const data = [ - { - id: 1, - teamName: "Anonymized_Team_38121", - assignment: "Assignment 1", - members: [ - { name: "Student 10566", id: 10566 }, - { name: "Student 10559", id: 10559 }, - { name: "Student 10359", id: 10359 }, - ], - links: [ - { url: "https://github.com/example/repo", displayName: "GitHub Repository" }, - { url: "http://google.com", displayName: "Submission Link" }, - ], - fileInfo: [ - { name: "README.md", size: "14.9 KB", dateModified: "2024-10-03 23:36:57" }, - ], - }, - { - id: 2, - teamName: "Anonymized_Team_38122", - assignment: "Assignment 2", - members: [ - { name: "Student 10593", id: 10593 }, - { name: "Student 10623", id: 10623 }, - ], - links: [ - { url: "https://github.com/example/repo2", displayName: "GitHub Repository" }, - ], - fileInfo: [ - { name: "README.md", size: "11.7 KB", dateModified: "2024-10-01 12:15:00" }, - ], - }, - ]; + const date = new Date(Date.parse('04 Dec 2021 00:12:00 GMT')); + const data = Array.from({ length: 23 }, (_, i) => { + const id = i + 1; + const teamNumber = 38121 + i; + const assignmentNumber = (i % 5) + 1; + const studentCount = (i % 3) + 1; + const currentDate = new Date(new Date().setDate(date.getDate() + i)); + + const members = Array.from({ length: studentCount }, (_, j) => ({ + name: `Student ${10000 + i * 10 + j}`, + id: 10000 + i * 10 + j, + })); + + const links = [ + { url: `https://github.com/example/repo${id}`, displayName: "GitHub Repository" }, + { url: `http://example.com/submission${id}`, displayName: "Submission Link" }, + ]; + + const fileInfo = [ + { + name: `README.md`, + size: `${(Math.random() * 15 + 10).toFixed(1)} KB`, + dateModified: formatDate(currentDate), + }, + ]; + + return { + id, + teamName: `Anonymized_Team_${teamNumber}`, + assignment: `Assignment ${assignmentNumber}`, + members, + links, + fileInfo, + }; + }); setSubmissions(data); setFilteredSubmissions(data); @@ -55,6 +56,20 @@ const SubmissionView = () => { fetchSubmissions(); }, []); + const formatDate = (date: Date) => { + const padZero = (num: number) => String(num).padStart(2, '0'); + + const year = String(date.getFullYear()) // Last two digits of the year + const month = padZero(date.getMonth() + 1); // Months are zero-based, so we add 1 + const day = padZero(date.getDate()); + + const hours = padZero(date.getHours()); + const minutes = padZero(date.getMinutes()); + const seconds = padZero(date.getSeconds()); + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; + } + const handleGradeClick = (id: number) => { console.log(`Assign Grade clicked for submission ID ${id}`); }; From d86cf1b0677dc41d588c34aea85d4198f9ad31df Mon Sep 17 00:00:00 2001 From: mjfeng Date: Sun, 27 Oct 2024 23:52:44 -0400 Subject: [PATCH 025/100] submission history view --- package-lock.json | 45 +++++- src/App.tsx | 5 + .../SubmissionHistoryView.test.tsx | 1 + .../Submissions/SubmissionHistoryView.tsx | 136 ++++++++++++++++++ .../SubmissionTable/SubmissionEntry.tsx | 2 +- 5 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 src/pages/Submissions/SubmissionHistoryView.test.tsx create mode 100644 src/pages/Submissions/SubmissionHistoryView.tsx diff --git a/package-lock.json b/package-lock.json index 3ef4561e..19bc6dc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@types/react-router-dom": "^5.3.3", "axios": "^1.4.0", "bootstrap": "^5.3.3", - "chart.js": "^3.7.0", + "chart.js": "^4.1.1", "formik": "^2.2.9", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", @@ -38,7 +38,7 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", - "recharts": "^2.12.3", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", @@ -3045,6 +3045,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -5760,9 +5766,16 @@ } }, "node_modules/chart.js": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz", - "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg==" + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.5.tgz", + "integrity": "sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } }, "node_modules/check-types": { "version": "11.2.3", @@ -16603,6 +16616,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/src/App.tsx b/src/App.tsx index 7e07e27a..83b6f354 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import EditProfile from "pages/Profile/Edit"; import Reviews from "pages/Reviews/reviews"; import SubmissionsView from "pages/Submissions/SubmissionsView"; import SubmissionView from "pages/Submissions/SubmissionView"; +import SubmissionHistoryView from "./pages/Submissions/SubmissionHistoryView"; import TA from "pages/TA/TA"; import TAEditor from "pages/TA/TAEditor"; import { loadTAs } from "pages/TA/TAUtil"; @@ -75,6 +76,10 @@ function App() { element: , loader: loadAssignment, }, + { + path: "submissions/history/:submissionId", + element: } leastPrivilegeRole={ROLE.TA} />, + }, { path: "assignments/edit/:id/viewscores", element: , diff --git a/src/pages/Submissions/SubmissionHistoryView.test.tsx b/src/pages/Submissions/SubmissionHistoryView.test.tsx new file mode 100644 index 00000000..5040fd93 --- /dev/null +++ b/src/pages/Submissions/SubmissionHistoryView.test.tsx @@ -0,0 +1 @@ +// WIP \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx new file mode 100644 index 00000000..4f16bdf6 --- /dev/null +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -0,0 +1,136 @@ +import { useEffect, useState } from 'react'; +import { Container } from 'react-bootstrap'; +import { useParams } from 'react-router-dom'; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table'; + +interface HistoryEntry { + teamId: number; + operation: string; + user: string; + content: string; + created: string; +} + +const SubmissionHistoryView = () => { + const [history, setHistory] = useState([]); + + // Does nothing at the moment but a real implementation would likely + // retrieve submission history data via the submission ID + const { submissionId } = useParams(); + + const columnHelper = createColumnHelper(); + + const columns = [ + columnHelper.accessor('teamId', { + header: 'Team Id', + cell: info => info.getValue(), + }), + columnHelper.accessor('operation', { + header: 'Operation', + cell: info => info.getValue(), + }), + columnHelper.accessor('user', { + header: 'User', + cell: info => info.getValue(), + }), + columnHelper.accessor('content', { + header: 'Content', + cell: info => info.getValue(), + }), + columnHelper.accessor('created', { + header: 'Created', + cell: info => info.getValue(), + }), + ]; + + // Load data, dummy data for now + useEffect(() => { + const dummyData: HistoryEntry[] = [ + { + teamId: 38121, + operation: 'Submit Hyperlink', + user: 'adgorkar', + content: 'https://github.ncsu.edu/adgorkar/CSC_ECE_517_Fall2024_Program_2', + created: '2024-09-17 22:38:09 -0400' + }, + { + teamId: 38121, + operation: 'Submit Hyperlink', + user: 'adgorkar', + content: 'http://152.7.176.240:8080/', + created: '2024-09-27 18:32:10 -0400' + }, + { + teamId: 38121, + operation: 'Submit File', + user: 'adgorkar', + content: 'README.md', + created: '2024-09-29 17:52:24 -0400' + }, + { + teamId: 38121, + operation: 'Remove File', + user: 'adgorkar', + content: 'README.md', + created: '2024-10-03 23:36:03 -0400' + }, + { + teamId: 38121, + operation: 'Submit File', + user: 'adgorkar', + content: 'README_4_.md', + created: '2024-10-03 23:36:57 -0400' + } + ]; + setHistory(dummyData); + }, [submissionId]); + + const table = useReactTable({ + data: history, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( + +
+
+ + + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ))} + +
+

Submission Record

+
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ + + ); +}; + +export default SubmissionHistoryView; \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx index 296f3759..90842088 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx @@ -102,7 +102,7 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void id: 'history', header: () => 'History', cell: (info) => ( - History + History ), enableSorting: false, enableColumnFilter: false, From 5ee8c4a16a1c9288aae06e57c1b48849b139a97e Mon Sep 17 00:00:00 2001 From: mjfeng Date: Sun, 27 Oct 2024 23:58:31 -0400 Subject: [PATCH 026/100] Revert "submission history view" This reverts commit 1eb5d7b8d677ab083a999119abc1434b812e4a5a. --- package-lock.json | 45 +----- src/App.tsx | 5 - .../SubmissionHistoryView.test.tsx | 1 - .../Submissions/SubmissionHistoryView.tsx | 136 ------------------ .../SubmissionTable/SubmissionEntry.tsx | 2 +- 5 files changed, 6 insertions(+), 183 deletions(-) delete mode 100644 src/pages/Submissions/SubmissionHistoryView.test.tsx delete mode 100644 src/pages/Submissions/SubmissionHistoryView.tsx diff --git a/package-lock.json b/package-lock.json index 19bc6dc8..3ef4561e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@types/react-router-dom": "^5.3.3", "axios": "^1.4.0", "bootstrap": "^5.3.3", - "chart.js": "^4.1.1", + "chart.js": "^3.7.0", "formik": "^2.2.9", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", @@ -38,7 +38,7 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", - "recharts": "^2.0.0", + "recharts": "^2.12.3", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", @@ -3045,12 +3045,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@kurkle/color": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", - "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", - "license": "MIT" - }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -5766,16 +5760,9 @@ } }, "node_modules/chart.js": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.5.tgz", - "integrity": "sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==", - "license": "MIT", - "dependencies": { - "@kurkle/color": "^0.3.0" - }, - "engines": { - "pnpm": ">=8" - } + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz", + "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg==" }, "node_modules/check-types": { "version": "11.2.3", @@ -16616,28 +16603,6 @@ "node": ">= 0.8" } }, - "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", - "license": "MIT AND ISC", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/src/App.tsx b/src/App.tsx index 83b6f354..7e07e27a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,7 +17,6 @@ import EditProfile from "pages/Profile/Edit"; import Reviews from "pages/Reviews/reviews"; import SubmissionsView from "pages/Submissions/SubmissionsView"; import SubmissionView from "pages/Submissions/SubmissionView"; -import SubmissionHistoryView from "./pages/Submissions/SubmissionHistoryView"; import TA from "pages/TA/TA"; import TAEditor from "pages/TA/TAEditor"; import { loadTAs } from "pages/TA/TAUtil"; @@ -76,10 +75,6 @@ function App() { element: , loader: loadAssignment, }, - { - path: "submissions/history/:submissionId", - element: } leastPrivilegeRole={ROLE.TA} />, - }, { path: "assignments/edit/:id/viewscores", element: , diff --git a/src/pages/Submissions/SubmissionHistoryView.test.tsx b/src/pages/Submissions/SubmissionHistoryView.test.tsx deleted file mode 100644 index 5040fd93..00000000 --- a/src/pages/Submissions/SubmissionHistoryView.test.tsx +++ /dev/null @@ -1 +0,0 @@ -// WIP \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx deleted file mode 100644 index 4f16bdf6..00000000 --- a/src/pages/Submissions/SubmissionHistoryView.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Container } from 'react-bootstrap'; -import { useParams } from 'react-router-dom'; -import { - createColumnHelper, - flexRender, - getCoreRowModel, - useReactTable, -} from '@tanstack/react-table'; - -interface HistoryEntry { - teamId: number; - operation: string; - user: string; - content: string; - created: string; -} - -const SubmissionHistoryView = () => { - const [history, setHistory] = useState([]); - - // Does nothing at the moment but a real implementation would likely - // retrieve submission history data via the submission ID - const { submissionId } = useParams(); - - const columnHelper = createColumnHelper(); - - const columns = [ - columnHelper.accessor('teamId', { - header: 'Team Id', - cell: info => info.getValue(), - }), - columnHelper.accessor('operation', { - header: 'Operation', - cell: info => info.getValue(), - }), - columnHelper.accessor('user', { - header: 'User', - cell: info => info.getValue(), - }), - columnHelper.accessor('content', { - header: 'Content', - cell: info => info.getValue(), - }), - columnHelper.accessor('created', { - header: 'Created', - cell: info => info.getValue(), - }), - ]; - - // Load data, dummy data for now - useEffect(() => { - const dummyData: HistoryEntry[] = [ - { - teamId: 38121, - operation: 'Submit Hyperlink', - user: 'adgorkar', - content: 'https://github.ncsu.edu/adgorkar/CSC_ECE_517_Fall2024_Program_2', - created: '2024-09-17 22:38:09 -0400' - }, - { - teamId: 38121, - operation: 'Submit Hyperlink', - user: 'adgorkar', - content: 'http://152.7.176.240:8080/', - created: '2024-09-27 18:32:10 -0400' - }, - { - teamId: 38121, - operation: 'Submit File', - user: 'adgorkar', - content: 'README.md', - created: '2024-09-29 17:52:24 -0400' - }, - { - teamId: 38121, - operation: 'Remove File', - user: 'adgorkar', - content: 'README.md', - created: '2024-10-03 23:36:03 -0400' - }, - { - teamId: 38121, - operation: 'Submit File', - user: 'adgorkar', - content: 'README_4_.md', - created: '2024-10-03 23:36:57 -0400' - } - ]; - setHistory(dummyData); - }, [submissionId]); - - const table = useReactTable({ - data: history, - columns, - getCoreRowModel: getCoreRowModel(), - }); - - return ( - -
- - - - - - {table.getHeaderGroups().map(headerGroup => ( - - {headerGroup.headers.map(header => ( - - ))} - - ))} - - - {table.getRowModel().rows.map(row => ( - - {row.getVisibleCells().map(cell => ( - - ))} - - ))} - -
-

Submission Record

-
- {flexRender(header.column.columnDef.header, header.getContext())} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
-
- ); -}; - -export default SubmissionHistoryView; \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx index 90842088..296f3759 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx @@ -102,7 +102,7 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void id: 'history', header: () => 'History', cell: (info) => ( - History + History ), enableSorting: false, enableColumnFilter: false, From 1af3b6f6f37b76ebee591c0d45d368b341f361a6 Mon Sep 17 00:00:00 2001 From: mjfeng Date: Mon, 28 Oct 2024 00:05:04 -0400 Subject: [PATCH 027/100] woops made a mistake, changes without the updated package file --- src/App.tsx | 5 + .../SubmissionHistoryView.test.tsx.wip | 1 + .../Submissions/SubmissionHistoryView.tsx | 136 ++++++++++++++++++ .../SubmissionTable/SubmissionEntry.tsx | 2 +- 4 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 src/pages/Submissions/SubmissionHistoryView.test.tsx.wip create mode 100644 src/pages/Submissions/SubmissionHistoryView.tsx diff --git a/src/App.tsx b/src/App.tsx index 7e07e27a..83b6f354 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import EditProfile from "pages/Profile/Edit"; import Reviews from "pages/Reviews/reviews"; import SubmissionsView from "pages/Submissions/SubmissionsView"; import SubmissionView from "pages/Submissions/SubmissionView"; +import SubmissionHistoryView from "./pages/Submissions/SubmissionHistoryView"; import TA from "pages/TA/TA"; import TAEditor from "pages/TA/TAEditor"; import { loadTAs } from "pages/TA/TAUtil"; @@ -75,6 +76,10 @@ function App() { element: , loader: loadAssignment, }, + { + path: "submissions/history/:submissionId", + element: } leastPrivilegeRole={ROLE.TA} />, + }, { path: "assignments/edit/:id/viewscores", element: , diff --git a/src/pages/Submissions/SubmissionHistoryView.test.tsx.wip b/src/pages/Submissions/SubmissionHistoryView.test.tsx.wip new file mode 100644 index 00000000..5040fd93 --- /dev/null +++ b/src/pages/Submissions/SubmissionHistoryView.test.tsx.wip @@ -0,0 +1 @@ +// WIP \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx new file mode 100644 index 00000000..4f16bdf6 --- /dev/null +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -0,0 +1,136 @@ +import { useEffect, useState } from 'react'; +import { Container } from 'react-bootstrap'; +import { useParams } from 'react-router-dom'; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table'; + +interface HistoryEntry { + teamId: number; + operation: string; + user: string; + content: string; + created: string; +} + +const SubmissionHistoryView = () => { + const [history, setHistory] = useState([]); + + // Does nothing at the moment but a real implementation would likely + // retrieve submission history data via the submission ID + const { submissionId } = useParams(); + + const columnHelper = createColumnHelper(); + + const columns = [ + columnHelper.accessor('teamId', { + header: 'Team Id', + cell: info => info.getValue(), + }), + columnHelper.accessor('operation', { + header: 'Operation', + cell: info => info.getValue(), + }), + columnHelper.accessor('user', { + header: 'User', + cell: info => info.getValue(), + }), + columnHelper.accessor('content', { + header: 'Content', + cell: info => info.getValue(), + }), + columnHelper.accessor('created', { + header: 'Created', + cell: info => info.getValue(), + }), + ]; + + // Load data, dummy data for now + useEffect(() => { + const dummyData: HistoryEntry[] = [ + { + teamId: 38121, + operation: 'Submit Hyperlink', + user: 'adgorkar', + content: 'https://github.ncsu.edu/adgorkar/CSC_ECE_517_Fall2024_Program_2', + created: '2024-09-17 22:38:09 -0400' + }, + { + teamId: 38121, + operation: 'Submit Hyperlink', + user: 'adgorkar', + content: 'http://152.7.176.240:8080/', + created: '2024-09-27 18:32:10 -0400' + }, + { + teamId: 38121, + operation: 'Submit File', + user: 'adgorkar', + content: 'README.md', + created: '2024-09-29 17:52:24 -0400' + }, + { + teamId: 38121, + operation: 'Remove File', + user: 'adgorkar', + content: 'README.md', + created: '2024-10-03 23:36:03 -0400' + }, + { + teamId: 38121, + operation: 'Submit File', + user: 'adgorkar', + content: 'README_4_.md', + created: '2024-10-03 23:36:57 -0400' + } + ]; + setHistory(dummyData); + }, [submissionId]); + + const table = useReactTable({ + data: history, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( + +
+ + + + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ))} + +
+

Submission Record

+
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+
+ ); +}; + +export default SubmissionHistoryView; \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx index 296f3759..90842088 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx @@ -102,7 +102,7 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void id: 'history', header: () => 'History', cell: (info) => ( - History + History ), enableSorting: false, enableColumnFilter: false, From f2a1cea0e8643d2719ec3465993aa316c6670745 Mon Sep 17 00:00:00 2001 From: mjfeng Date: Tue, 29 Oct 2024 15:09:50 -0400 Subject: [PATCH 028/100] submission history tests --- .../SubmissionHistoryView.test.tsx | 81 +++++++++++++++++++ .../SubmissionHistoryView.test.tsx.wip | 1 - 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/pages/Submissions/SubmissionHistoryView.test.tsx delete mode 100644 src/pages/Submissions/SubmissionHistoryView.test.tsx.wip diff --git a/src/pages/Submissions/SubmissionHistoryView.test.tsx b/src/pages/Submissions/SubmissionHistoryView.test.tsx new file mode 100644 index 00000000..11b1c55b --- /dev/null +++ b/src/pages/Submissions/SubmissionHistoryView.test.tsx @@ -0,0 +1,81 @@ +// SubmissionHistoryView.test.tsx +import '@testing-library/jest-dom'; +import { render, screen, within } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import SubmissionHistoryView from './SubmissionHistoryView'; + +// Mock useParams with different submission IDs for testing +const mockUseParams = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => mockUseParams() +})); + +const renderWithRouter = (component: React.ReactNode) => { + return render( + + {component} + + ); +}; + +describe('SubmissionHistoryView', () => { + beforeEach(() => { + // Reset the mock before each test + mockUseParams.mockReset(); + // Default mock return value + mockUseParams.mockReturnValue({ submissionId: '38121' }); + }); + + // Add this new test + test('receives correct submission ID from URL parameters', () => { + // Set up mock to return a specific submission ID + mockUseParams.mockReturnValue({ submissionId: '12345' }); + + renderWithRouter(); + + // Verify that the mock was called + expect(mockUseParams).toHaveBeenCalled(); + + // Get the value that useParams returned + const { submissionId } = mockUseParams(); + expect(submissionId).toBe('12345'); + }); + + test('renders submission record title', () => { + renderWithRouter(); + expect(screen.getByText('Submission Record')).toBeInTheDocument(); + }); + + test('renders table headers', () => { + renderWithRouter(); + expect(screen.getByText('Team Id')).toBeInTheDocument(); + expect(screen.getByText('Operation')).toBeInTheDocument(); + expect(screen.getByText('User')).toBeInTheDocument(); + expect(screen.getByText('Content')).toBeInTheDocument(); + expect(screen.getByText('Created')).toBeInTheDocument(); + }); + + test('displays dummy data correctly', () => { + renderWithRouter(); + + // Get all rows (excluding header rows) + const rows = screen.getAllByRole('row').slice(2); // Skip title and header rows + + // Test first row data + const firstRow = rows[0]; + const cells = within(firstRow).getAllByRole('cell'); + + expect(cells[0]).toHaveTextContent('38121'); + expect(cells[1]).toHaveTextContent('Submit Hyperlink'); + expect(cells[2]).toHaveTextContent('adgorkar'); + expect(cells[3]).toHaveTextContent('https://github.ncsu.edu/adgorkar/CSC_ECE_517_Fall2024_Program_2'); + expect(cells[4]).toHaveTextContent('2024-09-17 22:38:09 -0400'); + }); + + test('renders correct number of rows', () => { + renderWithRouter(); + const rows = screen.getAllByRole('row'); + expect(rows.length).toBe(7); // 5 data rows + 2 header rows + }); +}); \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionHistoryView.test.tsx.wip b/src/pages/Submissions/SubmissionHistoryView.test.tsx.wip deleted file mode 100644 index 5040fd93..00000000 --- a/src/pages/Submissions/SubmissionHistoryView.test.tsx.wip +++ /dev/null @@ -1 +0,0 @@ -// WIP \ No newline at end of file From b6d81d85c4873ffe97c2bd46373a6f38203faa0f Mon Sep 17 00:00:00 2001 From: masonhorne Date: Tue, 29 Oct 2024 18:16:57 -0400 Subject: [PATCH 029/100] remove user link --- src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx index 90842088..373bdb0f 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx @@ -48,9 +48,10 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void cell: (info) => info.getValue().map((member) => (
- + {/* This can be used to link to the users profile once the profile component exists */} + {/* */} {member.name} (Student {member.id}) - + {/* */}
)), size: 35, From 950ec834fa39adeda08f9778cb8aaec9d8d0921f Mon Sep 17 00:00:00 2001 From: mjfeng Date: Tue, 29 Oct 2024 19:31:40 -0400 Subject: [PATCH 030/100] test changes --- .../SubmissionHistoryView.test.tsx | 39 +++++++------------ .../Submissions/SubmissionHistoryView.tsx | 32 +++++++-------- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/src/pages/Submissions/SubmissionHistoryView.test.tsx b/src/pages/Submissions/SubmissionHistoryView.test.tsx index 11b1c55b..e85b8f21 100644 --- a/src/pages/Submissions/SubmissionHistoryView.test.tsx +++ b/src/pages/Submissions/SubmissionHistoryView.test.tsx @@ -1,10 +1,8 @@ -// SubmissionHistoryView.test.tsx import '@testing-library/jest-dom'; import { render, screen, within } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; import SubmissionHistoryView from './SubmissionHistoryView'; -// Mock useParams with different submission IDs for testing const mockUseParams = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -21,25 +19,17 @@ const renderWithRouter = (component: React.ReactNode) => { describe('SubmissionHistoryView', () => { beforeEach(() => { - // Reset the mock before each test mockUseParams.mockReset(); - // Default mock return value - mockUseParams.mockReturnValue({ submissionId: '38121' }); + mockUseParams.mockReturnValue({ submissionId: '1' }); }); - // Add this new test + // Check if Submission ID is correct test('receives correct submission ID from URL parameters', () => { - // Set up mock to return a specific submission ID - mockUseParams.mockReturnValue({ submissionId: '12345' }); - + mockUseParams.mockReturnValue({ submissionId: '1' }); renderWithRouter(); - - // Verify that the mock was called expect(mockUseParams).toHaveBeenCalled(); - - // Get the value that useParams returned const { submissionId } = mockUseParams(); - expect(submissionId).toBe('12345'); + expect(submissionId).toBe('1'); }); test('renders submission record title', () => { @@ -47,8 +37,10 @@ describe('SubmissionHistoryView', () => { expect(screen.getByText('Submission Record')).toBeInTheDocument(); }); + // Check if table renders properly test('renders table headers', () => { renderWithRouter(); + expect(screen.getByText('Team Id')).toBeInTheDocument(); expect(screen.getByText('Operation')).toBeInTheDocument(); expect(screen.getByText('User')).toBeInTheDocument(); @@ -56,26 +48,25 @@ describe('SubmissionHistoryView', () => { expect(screen.getByText('Created')).toBeInTheDocument(); }); - test('displays dummy data correctly', () => { + // Check if data is displayed correctly + test('displays data correctly', () => { renderWithRouter(); - // Get all rows (excluding header rows) - const rows = screen.getAllByRole('row').slice(2); // Skip title and header rows - - // Test first row data + const rows = screen.getAllByRole('row').slice(2); const firstRow = rows[0]; const cells = within(firstRow).getAllByRole('cell'); - expect(cells[0]).toHaveTextContent('38121'); + expect(cells[0]).toHaveTextContent('12345'); expect(cells[1]).toHaveTextContent('Submit Hyperlink'); - expect(cells[2]).toHaveTextContent('adgorkar'); - expect(cells[3]).toHaveTextContent('https://github.ncsu.edu/adgorkar/CSC_ECE_517_Fall2024_Program_2'); - expect(cells[4]).toHaveTextContent('2024-09-17 22:38:09 -0400'); + expect(cells[2]).toHaveTextContent('Test_User'); + expect(cells[3]).toHaveTextContent('https://github.ncsu.edu/masonhorne/reimplementation-front-end'); + expect(cells[4]).toHaveTextContent('2024-09-17 22:38:09'); }); + // Check if rows are displayed correctly test('renders correct number of rows', () => { renderWithRouter(); const rows = screen.getAllByRole('row'); - expect(rows.length).toBe(7); // 5 data rows + 2 header rows + expect(rows.length).toBe(7); }); }); \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx index 4f16bdf6..e0695a41 100644 --- a/src/pages/Submissions/SubmissionHistoryView.tsx +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -52,39 +52,39 @@ const SubmissionHistoryView = () => { useEffect(() => { const dummyData: HistoryEntry[] = [ { - teamId: 38121, + teamId: 12345, operation: 'Submit Hyperlink', - user: 'adgorkar', - content: 'https://github.ncsu.edu/adgorkar/CSC_ECE_517_Fall2024_Program_2', - created: '2024-09-17 22:38:09 -0400' + user: 'Test_User', + content: 'https://github.ncsu.edu/masonhorne/reimplementation-front-end', + created: '2024-09-17 22:38:09' }, { - teamId: 38121, + teamId: 12345, operation: 'Submit Hyperlink', - user: 'adgorkar', + user: 'Test_User', content: 'http://152.7.176.240:8080/', - created: '2024-09-27 18:32:10 -0400' + created: '2024-09-27 18:32:10' }, { - teamId: 38121, + teamId: 12345, operation: 'Submit File', - user: 'adgorkar', + user: 'Test_User', content: 'README.md', - created: '2024-09-29 17:52:24 -0400' + created: '2024-09-29 17:52:24' }, { - teamId: 38121, + teamId: 12345, operation: 'Remove File', - user: 'adgorkar', + user: 'Test_User', content: 'README.md', - created: '2024-10-03 23:36:03 -0400' + created: '2024-10-03 23:36:03' }, { - teamId: 38121, + teamId: 12345, operation: 'Submit File', - user: 'adgorkar', + user: 'Test_User', content: 'README_4_.md', - created: '2024-10-03 23:36:57 -0400' + created: '2024-10-03 23:36:57' } ]; setHistory(dummyData); From 00273b059f6b4edf8888cc68b3d23a528f1afe96 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Thu, 14 Nov 2024 15:40:43 -0500 Subject: [PATCH 031/100] update submissions and links to single column --- .../SubmissionTable/SubmissionEntry.tsx | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx index 373bdb0f..6f4b9fc8 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx @@ -59,45 +59,39 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void enableColumnFilter: false, enableGlobalFilter: false, }), - - // Links column: No search, no sorting - columnHelper.accessor('links', { + // Links and File Info column: No search, no sorting + columnHelper.accessor(row => ({ links: row.links, fileInfo: row.fileInfo }), { + id: 'links', header: () => 'Links', cell: (info) => ( +
+ {info.getValue().links.map((link, idx) => ( + + ))} +
- {info.getValue().map((link, idx) => ( - - ))} -
- ), - size: 15, - enableSorting: false, - enableColumnFilter: false, - enableGlobalFilter: false, - }), - - // File Info column: No search, no sorting - columnHelper.accessor('fileInfo', { - header: () => 'File Info', - cell: (info) => ( -
- {info.getValue().map((file, idx) => ( -
-
{file.name}
-
Size: {file.size}
-
Date Modified: {file.dateModified}
+
+
Name
+
Size
+
Date Modified
+
+ {info.getValue().fileInfo.map((file, idx) => ( +
+
{file.name}
+
{file.size}
+
{file.dateModified}
))}
+
), - size: 25, + size: 40, enableSorting: false, enableColumnFilter: false, enableGlobalFilter: false, }), - // History column: Links to history pages (No search or sorting) columnHelper.display({ id: 'history', From 88ad8536b09aff8b5369d9b49b84a593339c4d69 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Thu, 14 Nov 2024 15:51:26 -0500 Subject: [PATCH 032/100] update submissions header to be above table --- src/pages/Submissions/SubmissionsView.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/Submissions/SubmissionsView.tsx b/src/pages/Submissions/SubmissionsView.tsx index 1ed53ba5..1f595c01 100644 --- a/src/pages/Submissions/SubmissionsView.tsx +++ b/src/pages/Submissions/SubmissionsView.tsx @@ -85,11 +85,15 @@ const SubmissionView = () => { }; return ( - + - +

Submissions


+ +
+ + Filter by Assignment handleAssignmentChange(e as any)}> From 4b2fc8987ff67868fa2ef6e16df2bc38975e8264 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Thu, 14 Nov 2024 15:52:16 -0500 Subject: [PATCH 033/100] update submission history header --- src/pages/Submissions/SubmissionHistoryView.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx index e0695a41..f42764d2 100644 --- a/src/pages/Submissions/SubmissionHistoryView.tsx +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -1,12 +1,12 @@ -import { useEffect, useState } from 'react'; -import { Container } from 'react-bootstrap'; -import { useParams } from 'react-router-dom'; import { createColumnHelper, flexRender, getCoreRowModel, useReactTable, } from '@tanstack/react-table'; +import { useEffect, useState } from 'react'; +import { Container } from 'react-bootstrap'; +import { useParams } from 'react-router-dom'; interface HistoryEntry { teamId: number; @@ -103,7 +103,7 @@ const SubmissionHistoryView = () => { -

Submission Record

+

Submission History

{table.getHeaderGroups().map(headerGroup => ( From d8b5062b2f7a3581f5931a559e582727daf47da8 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Tue, 18 Mar 2025 16:30:21 -0400 Subject: [PATCH 034/100] text format changes to submissions --- src/pages/Submissions/SubmissionTable/SubmissionList.tsx | 2 +- src/pages/Submissions/SubmissionsView.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Submissions/SubmissionTable/SubmissionList.tsx b/src/pages/Submissions/SubmissionTable/SubmissionList.tsx index f0a65f0f..d8fb1ba1 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionList.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionList.tsx @@ -7,7 +7,7 @@ const SubmissionList = ({ submissions, onGradeClick }: { submissions: any[], onG const columns = useMemo(() => SubmissionEntry({ onGradeClick }), [onGradeClick]); return ( -
+
{ -

Submissions

+

Submissions


From ef357d798d14cf1987c3e40af48b5dea0f6adca9 Mon Sep 17 00:00:00 2001 From: aryansharma2k2 Date: Tue, 18 Mar 2025 16:50:09 -0400 Subject: [PATCH 035/100] Updating Submission History Table to standard table format. --- .../Submissions/SubmissionHistoryView.tsx | 79 +++++++------------ 1 file changed, 27 insertions(+), 52 deletions(-) diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx index f42764d2..d092c80a 100644 --- a/src/pages/Submissions/SubmissionHistoryView.tsx +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -1,12 +1,12 @@ import { createColumnHelper, + useReactTable, flexRender, getCoreRowModel, - useReactTable, } from '@tanstack/react-table'; import { useEffect, useState } from 'react'; -import { Container } from 'react-bootstrap'; import { useParams } from 'react-router-dom'; +import Table from '../../components/Table/Table'; // Make sure this is correctly imported interface HistoryEntry { teamId: number; @@ -19,73 +19,72 @@ interface HistoryEntry { const SubmissionHistoryView = () => { const [history, setHistory] = useState([]); - // Does nothing at the moment but a real implementation would likely - // retrieve submission history data via the submission ID + // Fetch submissionId from URL params (if applicable) const { submissionId } = useParams(); - + const columnHelper = createColumnHelper(); const columns = [ columnHelper.accessor('teamId', { header: 'Team Id', - cell: info => info.getValue(), + cell: (info) => info.getValue(), }), columnHelper.accessor('operation', { header: 'Operation', - cell: info => info.getValue(), + cell: (info) => info.getValue(), }), columnHelper.accessor('user', { header: 'User', - cell: info => info.getValue(), + cell: (info) => info.getValue(), }), columnHelper.accessor('content', { header: 'Content', - cell: info => info.getValue(), + cell: (info) => info.getValue(), }), columnHelper.accessor('created', { header: 'Created', - cell: info => info.getValue(), + cell: (info) => info.getValue(), }), ]; - // Load data, dummy data for now + // Dummy data for now useEffect(() => { const dummyData: HistoryEntry[] = [ { teamId: 12345, operation: 'Submit Hyperlink', user: 'Test_User', - content: 'https://github.ncsu.edu/masonhorne/reimplementation-front-end', - created: '2024-09-17 22:38:09' + content: 'xyz', + created: '2024-09-17 22:38:09', }, { teamId: 12345, operation: 'Submit Hyperlink', user: 'Test_User', - content: 'http://152.7.176.240:8080/', - created: '2024-09-27 18:32:10' + content: 'xyzgh', + created: '2024-09-27 18:32:10', }, { teamId: 12345, operation: 'Submit File', user: 'Test_User', content: 'README.md', - created: '2024-09-29 17:52:24' + created: '2024-09-29 17:52:24', }, { teamId: 12345, operation: 'Remove File', user: 'Test_User', content: 'README.md', - created: '2024-10-03 23:36:03' + created: '2024-10-03 23:36:03', }, { teamId: 12345, operation: 'Submit File', user: 'Test_User', content: 'README_4_.md', - created: '2024-10-03 23:36:57' - } + created: '2024-10-03 23:36:57', + }, ]; setHistory(dummyData); }, [submissionId]); @@ -97,39 +96,15 @@ const SubmissionHistoryView = () => { }); return ( - -
-
- - - - - {table.getHeaderGroups().map(headerGroup => ( - - {headerGroup.headers.map(header => ( - - ))} - - ))} - - - {table.getRowModel().rows.map(row => ( - - {row.getVisibleCells().map(cell => ( - - ))} - - ))} - -
-

Submission History

-
- {flexRender(header.column.columnDef.header, header.getContext())} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
- +
+ = 10} + /> + ); }; From a72791ce9ce3a24a5b8b39a97b4787ae966e1e20 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Tue, 18 Mar 2025 17:03:02 -0400 Subject: [PATCH 036/100] fixed font size and line height --- src/pages/Submissions/SubmissionHistoryView.tsx | 2 +- src/pages/Submissions/SubmissionTable/SubmissionList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx index d092c80a..45a29c5c 100644 --- a/src/pages/Submissions/SubmissionHistoryView.tsx +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -96,7 +96,7 @@ const SubmissionHistoryView = () => { }); return ( -
+
SubmissionEntry({ onGradeClick }), [onGradeClick]); return ( -
+
Date: Tue, 18 Mar 2025 16:55:13 -0400 Subject: [PATCH 037/100] Updating path. --- src/pages/Submissions/SubmissionHistoryView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx index 45a29c5c..2451a7c9 100644 --- a/src/pages/Submissions/SubmissionHistoryView.tsx +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -6,7 +6,7 @@ import { } from '@tanstack/react-table'; import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; -import Table from '../../components/Table/Table'; // Make sure this is correctly imported +import Table from '../../components/Table/Table'; interface HistoryEntry { teamId: number; From 691ceaff46f95955e07c5a78cdea9fc380fa88d5 Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Fri, 1 Nov 2024 18:56:31 -0400 Subject: [PATCH 038/100] 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 f993a2105699e21d2a8fff71c48bda6a12b83a37 Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Fri, 1 Nov 2024 19:17:15 -0400 Subject: [PATCH 039/100] 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 zcmeHtcT`i&*Y1SS)Cej_6#)f7iik)HEvN_qDN>{t0i_8@iF62JrB{&>DN>|25ov-- zvjwFDrGz4g5G7RU@J)Q*@7}e(`|t1mao4(8D=Rs(=giF6v-h+2e&#$h(9_(%muD{k zU_Vytf*}BO@Gl);W`sY>{(T$phu-HL)|eT7f|(s3!0SCXwak40U}>X$5h>yS+W`S*oktQy4?4f|&yy<_$-A+6ep72% z%BNoqW7XtM}v0#5_eWtU#&Lt z^T+~bb|iRhR_kig-`d*h)V!KCFk`fNck(1QOvYpQB(;y&m79@HC^JY$x@sq&5@foHA#X%dmj{E>n}Dj z+1}o!2#4qyeryoY=}Da9mG>B?`h)=djjJHrQNp@m`ONgNywImQk9P@$4pm=8hK7d1 zuJh4luW67F7&!^c7dHq#b5vnn*dga6c~dqfCMJZBk5BDIEW%(|0g2Cj;rK4`rH@C1 zk4G6!M>T*-4O^c|ZJF=Q+VO2`V8D3UgKX#K-EC@Cyq3_blSS4Abs{-UZ2z1_k@d&<<*)KL5D-Ag~(?5p@nu9Q`8 z*dGTP_YipdaWQdm>MB{Pn&qTj%a_s77TwHm$eSE%M*#cXe4W&tG@m&RO6;-I6*)Pt zakp;GRk0upzWNv7Z`a-UR-KZ#gWYO+B1z5M?3x>C^fS8;M6HywbF*WbN2pkAZ>eSE z#|2;X$VZKEK;An5vg7NG#a8Mj#==xxpNbBOT~x)_waNlfvk#9XH%s$&T}LjropQNx zLwWGamxbNZ^ML*66TWPLkTE3xvy)O%VHXXdQWJt+EBejA>1-?ETFm_S0EVKbrmgO_ z*J`|{=ECiHyHtg6SN1a7jfIT=|YO+$?4(ETU7FP9*!CR<~289KQcW`m>A2_ zO<3ItGt&g35}62$di_MC_>TZ8>jSTJ_t0PN;k&MI?@!MQMBScf-gQ{;N!xddBP!$z znhrBQh5~y#s;_;xu7#G++nv@HI~pfHH&|OTgr>){7| z7#x^upp-w(%P3W%ySen$eZRm><)L%x4|1LB^DN7hv?e+s^@}RQa1(TVQB5ta8gomf$V$T^YHK9+S$igt~8t2Cttw5|{ zVP2Nf^s9_ZCw6D>?4gz0?Yd>uI@zJsdb-Pp8#C$bd6y;BCIT+WuJE1hX zPOO36M~p_`Wh|Qfy{=umHkGBlp@Q|I9A(cxI6ZvNSPa(*EjtnRvVk5iU%&WX+$rC% zNK?3ZsDYuqTX8k){{8!E8xXI?QsF~~#vt$m<`7|~W_&mHU~5T|q9-*8J_Em?Cu1oG zK%q34FRpUhQDbKco9y`Fi{9q{^XuD^I21MQKi~in(=b6h|L8#|)w7o17^?DLn9zno z6o`h3@S`jbZQ`vqVj?3W)t<+KRvqwprfwsE8YWo&M>8~2yRf&$|0L8EH_+%|3+AaLhTOfL1CsfzKYF`S;TTwD~Sv8=3p!sr=ygQwz zvFe|~i#_5k$M@N8dxmXzwwQEdT~kdP?KjFTpK?mrho5@`PEXd&&;^88^%M$8#+yb* zD95wOK+x37N-`bv>{rFwCiTj5Bbg5)1-iH1MxK2ZCt4=EXi?PLZ38CtFZI}Cqo!7b zTTI-&12HLsbLFP*(JWn@jF=~Do5lrqRCCm~{Q|7t%YBREhCSApa0#OYOp`0tSf8>WkeRBJOsdBXpI7FC_HM--J*P zGFAJo0f|9)4UX0QL{m^>R;K|md^A^haibuw3kDzchwo+tPoXYIUj>=@RdpNNPxJP| zIT(tp^y1c-)|U0znHXEm=PiwMv}^}vSdq~A&tP3tCa6X!IOcghF5vXmrKXAOKtN=R zS{2-pv(Tg=#5~Iu2eg$>Bt8)N-f4^D6)XMS7{t8X*7@S3Uei(~z%E!QwYe!ff=2p_ zqwsY}J*h&vl|*2Wd_t@zkitAxb{ddxkLHR`1=vdAql1S%zx0z1wx+?wx2lzTw=cKA zF_J-H&~~%7nG^7A0}4s%A?h}%7EVBPPt93uR&6XGKS0bg?x?1`quipf0O2+Y>(*Pb zIwG)%Ld;)0Y;nAZj7DOx^=8P1*JwI_Nyb*Uwfl$PQCMk1=cz5SE~y+7J%Yjy@bqa4 zeX53wOHA*pFoDYu=K>WeBpwPq5dJvROWH$gaM&`HHP;g_gZDmn+mf@k<3nhn@V4T; zBEsc84Dm)&+DixrQ4|Vq>RE)UFmVa5?Cl%%K{FtAN8?|MF?PRK9sTn`ZfsjQfr9Qe=LTT;q8b8*FUT- zxj4o*99zI3Pe)jhf&%JZ+HNm!^lv(V)>3T?K+S(eI-o$elpsLHb{_p|59W24Tj}&w z?V=F^x^-|0fkHHe{!R=+puCO2uGQ@}jt<^?lgE<)1hioIum_cI5MRftmU5I+KoWo6m77m7Rcj_~l|UY>_Pw?n5J(d5{7(Ib(UB>tV7B^tCKR zfVu;qpG$mKSju+bS(>a!85=%Z8qoQzYe-p$2pCYmJF%W_hlAw$5ezgkDInhV@Io@j zrLfm%TWY|4@h6ASnK=MVqHk|dR&Uc_YMG3|#E7=N1tqFF!J!Mli zKQc=)+u$^qN6WbB&3KltSEUy=I}&amlCF56pHZ>?kf&t*^U3UZ*yBY1;o}=0#KX#Y zGnCe%L1vx5M43L3+LKzok#@qA#c~?>Bc1r{K6xIdh6V99M@$9Izq|Mw9h{T+Gy%sF ziP2EbpOOh$Uhwnts@+w(DZjvlQhL2R4+Af8Oe-&BS5-r8)1;QAMM1qq**wSStyfHe z;zRaoILpG+(+1~rhN*p-eL+&em-mgX(D5(&JmLbC>|3n-NPs`g2qYGk_X9kTh6N1s z0VMeUfBzrQLwgw#ING9BFCR~d;o%#|ODH}OV=O0TB5gfmmLF`K?;D&Od{&RVaKNsp z#)zzN)y%LZFSTQ5F11f`hI@Kg2G(5gC#R#MBUR#f0xuEWnm+2fvya(Pf-k0bzU_mo z*;1&mn`-v>9x6u*xhr?NY4vxNS*c27E|t{Z-)zTs=Oe=|^J{>q(oXGv7B(xkqQB|B zU)Miw(qDr$AmPz-{hon|g*BKZv_~OY0Y=&@f$@}C`A$@M|JbDPV)@4g=BvDq`cBM* zbIV-6;YZ9bjkx)TiFRs)-bF>->uC+GPs60Pn3l!=hSUU0u!6^YejL zawzuroqcM>)10Ak5o+4+?mrbpfByC@AOgp16DI>;)qt3`Om&AWk`nw?aO`*$J{VDj6$DAgj)A6xnlJtvUIriX3)aJUgy zzC*_tLA)lC)2fL4EzFgGU}vXp--B~fN|nyN1z=Igyje@+)MoYMF$#P3=eDL*RWbxqWY*(53$#zesw2-NF>&i1as^FQ2gPrhwwP4qUVc0?LOVa<*ub} zHQQ@^hv?vZ4$U=G(!BqZKHxM{Jw84jLpmr(yQxnNm!fCgRjb>oxN*tUbO5&J$HKL1 ziFqXiN^Sd4P}!mZ)Ry4Mu7nv9Uw$-AkhYQPiwf7)F*KJ?(T1F8m+O#9GnsG;)ThY13rvYB#XI6s+ImO%w6a=moM=t%oQB3J9Ekm%i&v0Q7ee@qF+SIkR>lCOYF9 z0dc2b{hL!3!I|&!FE|V1MKLFBZxT1g+3HxrW2ohS8arjp;b^doO0|$@SAy#cPbf|! z?ZG_b0^IhH)Kl0#1n|L2WEpS-_$ zZAHD$ba8#X`QgKdNiL#vf5BDEa|A!pUz^v97Ay=54D2J33PLvjxcK{jMKd|&G&Nc6 z<2+@oqZ3u2n{l0xWVC<({@46(&m7!}=>(V)+Bkq>Wu;Vg9yJOIQtDoeGG^DXsItFn zVQFb{xqx^P_qLwGc#Y{5WdSlA(A-?q-fKKG_>!}cv87_$?U8vMTnj26dnD1 z@o~-ApHG73#>Z^WvGA*wHd0?@Y_fa4ft>2hvdr!ZWq>mIE{SxqtC~!*P7;_ zRk5^77EY~PNWlCR>b+i60(1WSdGyUOJdQCM4uR)W_Wz7VTzpA=4Jqc1l-==(S4_0d z57|UY+3Y62RymONobG@!xznfZ2;g``M8rQ0KmY@c&CbNc1YR4Oo4XJI%#{V~j0!GQ z>ERQKj0!D8{?2O_64H#Li|*_1H~**M>u|kK8JnFWxhde^u}Q_x@J+R#*r zD=G#u6@B`KhmU-`zjgY54RDSdU!g5sk+1#85 zLBZ=B5{cB#Hzc{JN9Afk3rat8EpnDZk@cIil9iV)q3!d2jn@CEL1TUWxWU1}=bodg z`&n5nr^h6cQ&Zi>-`sU|aryGxrMJ|_yErFDaLvK&#O;;^F3@$Mhy_1BepThqR|i*5 z&uR0}gveYiXdg6dc>n%A?0*{(&kd7@d9;d(%HVUCq}Zb>u7umDJj0W}hwF{*v+(r{ z*1DNkTe}nP(7n~b0~_3A*TE5=Zj~cjI$h&$@8ecS{Cu(|bRkd*5kh~ogl-2ClmmLu z4CbVvp_2W)a?dI%y3q^_cMt$`{P^*~rT`hH-9t2446nSmw1`bR0NI-SA;u#ku-fXX zA`BS2<@+@b(SXTHqB0#pM71VyGodjq@&pv$L}n6U&iGB3>E}Q5nI+3Z0XARR#J|>&rzo ztKn7X3o}2r&BxzpNm%=ZaQC-@b4;@6jU;3W^=Thk5DJTj0_j zd6))`CFKn?Lnm>I!Za8_H#addO2RREmlhRa%gV}N`vKSv*WKHIqZk_*IS_6iDJm}~ zN!AVL`YU@7Zqq>&6u_pRbxHJS{4J;ZMEM)s$}2`jPlr5P2;uh!t^(i>&4Do2w50dx zi3-d_&3pR#@|5OYMzHR@&QPELx6qO%95T{$AW@HUCzFehuRoSgX}OI}A8tVxnrT`v z@|bnVO20*OAnx6hKc3Fc%sX6<9*%j!vir>CV&vFi7$pW@g*5(|8~JcR^bk}{6sHvF zeTulwj%yer=K6nIHY?r~!akqEO^m+EN^(PEBW`QWYd;Dlnxbu9$RHVc47yd(ix*c` zR#wtwoMNG}Bi7^j|Mvi*NFaIvhP;~x1>nxx!>v6aKs#Uj5B!a^WCEhW?;H&`SE&Q9 zBbfRjBC(0+IsXqtf`Q^P3=KbsgpwaWuG99pFo*g7(BSOM%w1(;PAe;yb{-A71Ouk>2PwnL>@@qJ zSp!6S*dHR_TO9yw3SmsmkaO?(@>H|TC_da+ET5p{H~Tr~yhY=!vf`4GvLpk$m&^xR zOJa{G<_68x-;W5}qZqtaIrnPQW6r1vW}5iC-tMh|3fqb#5q*n7fe(Q}LA6)kGD4l3 z7iQy61K{lFcp*dH^T(L&8=B1P`^8f~+Df1!E&qHPSKQb*ghncozI}rmB?rwJMw|mL zpFBBQTL=AGK>yb7!FC-)JWTR#50FS5mfLiRmrm4%Y~K3U@+%<(zy!$LNuV2AONrwd zn1TNd!BFqhxs?DWKr=8XkhuTG?^`hCg1$KZH@tJ1?%a>sJW7k#B6^vX&{G{cWB)rm zx3=P;r=nO`{swT mQ=xyc23PEtUtwuwm!6dGT-@>kgUkX22yvfxshJxmRZ3iWFB zAi&_KQQ(bBV{YX$6fWQG$AE&?B!ptbF!Nos)`lKy%h!*58 z9Lrf>nzp3*J0`FL4Jr*UU(R9J$6*E^kwelV4?@FVT+_N=iUc~)0wQy}AWj_8NPr1J zsf+>O0@6evaHF?Cw>R>zIK&naQ)^s`tD74nte`L=&>|5Mni91yUKAG>!8cnyzPB`jg@Zw`pl-UoC8A{H23?Wk-jVarfw^< z{lYfa7>XpFvyuMudKLi{`o!F=qGhP8zu^SByF0^#7)a3 z)=RdzLE3l+@Ao>_vOFnEzwDRE8^B)!rs3g8rsX~({i4Q3%g)iS`Dp+!nbY0{h~poC z7GPMEjI;QhD{2Tg9O@5lXN98Ak7xDuijP^>VZCsV*E4#z_=I@*wD3axGX=LC6v#dx z4LwT(?=bAtCtl%c=?6eOE98SCT{3WV0IatH|1xcN&e8pl90JHO+2Iv!?1m2HVu3g0 zx-Xs&#OPU*xB-|vgdE#HJT061ob-1L9TZ-MyKqxT#hn1dXgQ73wywCUIO00;&n5Y` z<(5obv&5-}Vv^(om+99j(n4Gilha`TyRU3u_i zFB*5g$^ke&Y&6IEC57tu6t;0PGd#Uf`4)1_i9U1j|A^BXv9?C=%q-9#v2IS?qkWT= zR?tSa+h-!+N(GpPV>a?FpXz^bOUo-P4T@K~7ID9wh+HJX6Fe_Qf%7Q*5pQ|7xMJV> zOpg?JM9=h%lUJpvI`rs$ymF8?snoowFI|uk6I)M_{YCoufu~0}k)#zk*s$lD$-+cj zDI5NC@BliS{WMr?3$ZyqcS0pwtl840MRJB;XuqYOpwD3`Sg8bi6GOTia@Z9p`1th- z*XoIl$9cR$l8NlxBIC+=-`V=Nce1dtjjUY}$*ZB-ket#X3BoegKw#@FPldXqp~Z)( z=T<#a6DZv;YyF!?`#-m>LLxwLO%1{%|MOT;1}_T8{82t|68qaBY?8m{LcXk}i?x_b zY~DBT;-mri-iWYC+_tqxf3z*{z!EEjPF6_in9C>cB?4gnd1ORIF-2QN>g3fCy5!1n ze`VF?^71(bO1xp~$?F39PQ6;2u$dmND^tQBs*8vS`$MB67Ra789zZ*7bw{0Y&+v^7 zu^3k`GN=t-=>Z2H-jPe#lnjj&6M#J>o?ytD6Jb4_9}(r5b;oQB{=v05iKs zkmH+GT_CVxUL1kdX#FTUm)+~2Ks?gg+WJHD>Sz_brS&jWP4K!t3w#lvi$*d&hiBVx z9`^bQpZRV+$V?n3)(;6U5T;t!1I&AhDk}VGp&kh-V>@2*m`})Vn z3x}!OpC&GW_(gIUMB?!eBtgN6r+CD1_+CK?!Dlqy*ky!{i_4ohqTtXG4Oz}}Fq22& ze0_tv6j$RCPng$%faRXA3}H<_JRhV2Kzs6C374wX9XxJdyof$rs>YG6-|)b|0V714LR)8JdBgRifzD%0+px7?7jg=>-UBDyD@?QURDFePuo*N7-kA@v^` z7@Rt29c1O?=4PnW7OS4J#8|Uc;8dj3GstPNtbyrK2MIijf9UD zr!;LyQdbCq;Xw;BKKe)b__iPW%rRo4gfw8sbJ{&|O{=z1o!?n)gBRMHVOKTSl^)h0 zH@3#c#&RJsIjmSmb6g&N$8I0;<{vW&(y5{lXgg9COoBeN&MJ_MWMg~S_6t(n@aUhs z`HfhQ#(wYIF^=2*q(fuLeYJaZ9Kx~|$^1AXU!w16jwn93;pnq&q|#JfaRkhJ|1W;< 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 c50c66351d28e19130f1e88018b52315d673f344 Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Fri, 1 Nov 2024 19:22:24 -0400 Subject: [PATCH 040/100] 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 444457a3bcfe55132c34045dccdeaf3392a2314c Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Fri, 1 Nov 2024 19:31:26 -0400 Subject: [PATCH 041/100] 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 ffc5dba9d8a59bee2fbfb5bac939859cb5d7e910 Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Fri, 1 Nov 2024 19:50:30 -0400 Subject: [PATCH 042/100] 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 89ad0bea557b55f6f1f9436e3008741fa15cd200 Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Sat, 30 Nov 2024 23:05:06 -0500 Subject: [PATCH 043/100] 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 ea65b698cebe064d18a288f8695deb7acbd478ce Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Sat, 30 Nov 2024 23:27:55 -0500 Subject: [PATCH 044/100] 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 c40b8071b6346317fef14bf08278b83aba408620 Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Mon, 2 Dec 2024 16:53:38 -0500 Subject: [PATCH 045/100] 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 0e49df94f067e5c33ae0ecffa64152e748f146bb Mon Sep 17 00:00:00 2001 From: SurajRKU Date: Tue, 3 Dec 2024 21:13:49 -0500 Subject: [PATCH 046/100] 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 047/100] 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(); From cc2bb8ed32aea70f10f0371f9545478304a893ff Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Tue, 18 Mar 2025 17:27:46 -0400 Subject: [PATCH 048/100] updated date format --- src/pages/Courses/CourseColumns.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/Courses/CourseColumns.tsx b/src/pages/Courses/CourseColumns.tsx index 4aeb3393..277bac08 100644 --- a/src/pages/Courses/CourseColumns.tsx +++ b/src/pages/Courses/CourseColumns.tsx @@ -1,6 +1,7 @@ import { createColumnHelper, Row } from "@tanstack/react-table"; import { Button, Tooltip, OverlayTrigger, Badge } from "react-bootstrap"; import { ICourseResponse as ICourse } from "../../utils/interfaces"; +import { formatDate } from "./CourseUtil"; type Fn = (row: Row) => void; @@ -74,7 +75,7 @@ export const courseColumns = ( cell: (info) => (
- {new Date(info.getValue()).toLocaleDateString() || ( + {formatDate(new Date(info.getValue()).toLocaleDateString()) || ( N/A )} @@ -97,7 +98,7 @@ export const courseColumns = ( cell: (info) => (
- {new Date(info.getValue()).toLocaleDateString() || ( + {formatDate(new Date(info.getValue()).toLocaleDateString()) || ( N/A )} From cf649d5e5fc631446bbd79a4f36cdea8bfb1b955 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Tue, 18 Mar 2025 19:20:54 -0400 Subject: [PATCH 049/100] only show pagination if >10 courses --- src/pages/Courses/Course.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx index 29f402de..2f30fe31 100644 --- a/src/pages/Courses/Course.tsx +++ b/src/pages/Courses/Course.tsx @@ -164,6 +164,8 @@ const Courses = () => { ); }, [mergedTableData, loggedInUserRole]); + const showPagination = useMemo(() => visibleCourses.length > 10, [visibleCourses]); + return ( <> @@ -215,6 +217,7 @@ const Courses = () => { institution: auth.user.role === ROLE.SUPER_ADMIN.valueOf(), instructor: auth.user.role === ROLE.SUPER_ADMIN.valueOf(), }} + showPagination={showPagination} /> From 00bf86348d639c5045d65edf5894500addb1f748 Mon Sep 17 00:00:00 2001 From: aryansharma2k2 Date: Tue, 18 Mar 2025 17:21:06 -0400 Subject: [PATCH 050/100] Editing the create button. --- src/pages/Courses/Course.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx index 2f30fe31..cf1e0b8c 100644 --- a/src/pages/Courses/Course.tsx +++ b/src/pages/Courses/Course.tsx @@ -188,7 +188,7 @@ const Courses = () => {
- From d530d99f5c27e74297a4ebad36cb2a10bd1841d9 Mon Sep 17 00:00:00 2001 From: aryansharma2k2 Date: Tue, 18 Mar 2025 17:33:48 -0400 Subject: [PATCH 051/100] Formatting action buttons. --- src/pages/Courses/CourseColumns.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/Courses/CourseColumns.tsx b/src/pages/Courses/CourseColumns.tsx index 277bac08..0b70ae80 100644 --- a/src/pages/Courses/CourseColumns.tsx +++ b/src/pages/Courses/CourseColumns.tsx @@ -123,10 +123,10 @@ export const courseColumns = (
Edit Course}> - diff --git a/src/pages/Courses/CourseDelete.tsx b/src/pages/Courses/CourseDelete.tsx index be3023e7..ebfa24a3 100644 --- a/src/pages/Courses/CourseDelete.tsx +++ b/src/pages/Courses/CourseDelete.tsx @@ -65,10 +65,10 @@ const DeleteCourse: React.FC = ({ courseData, onClose }) => {

- - diff --git a/src/pages/Courses/CourseEditor.tsx b/src/pages/Courses/CourseEditor.tsx index c02a12da..4c932146 100644 --- a/src/pages/Courses/CourseEditor.tsx +++ b/src/pages/Courses/CourseEditor.tsx @@ -248,12 +248,12 @@ useEffect(() => { /> -
+
+
10} + /> + diff --git a/src/pages/Courses/CourseColumns.tsx b/src/pages/Courses/CourseColumns.tsx index 0b70ae80..70bdc6ae 100644 --- a/src/pages/Courses/CourseColumns.tsx +++ b/src/pages/Courses/CourseColumns.tsx @@ -15,110 +15,61 @@ export const courseColumns = ( handleCopy: Fn ) => [ columnHelper.accessor("name", { - id: "name", - header: () => ( - - Course Name - - ), + header: () => "Course name", cell: (info) => ( -
- {info.getValue()} +
+ {info.getValue()}
), enableSorting: true, - enableColumnFilter: true, - enableGlobalFilter: true, }), columnHelper.accessor("instructor.name", { - id: "instructor", - header: () => ( - - Instructor - - ), + header: () => "Instructor", cell: ({ row }) => { const instructor = row.original.instructor; return ( -
- - {instructor && instructor.name ? ( - instructor.name - ) : ( - Unassigned - )} - +
+ {instructor && instructor.name ? ( + instructor.name + ) : ( + Unassigned + )}
); }, enableSorting: true, - enableColumnFilter: true, - enableGlobalFilter: true, }), columnHelper.accessor("created_at", { - header: () => ( - - Creation Date - - ), + header: () => "Creation date", cell: (info) => ( -
- - {formatDate(new Date(info.getValue()).toLocaleDateString()) || ( +
+ { + formatDate(new Date(info.getValue()).toLocaleDateString()) || N/A - )} - + }
), enableSorting: true, - enableColumnFilter: true, - enableGlobalFilter: true, }), columnHelper.accessor("updated_at", { - header: () => ( - - Updated Date - - ), + header: () => "Updated date", cell: (info) => ( -
- - {formatDate(new Date(info.getValue()).toLocaleDateString()) || ( - N/A - )} - +
+ { + formatDate(new Date(info.getValue()).toLocaleDateString()) || + N/A + }
), enableSorting: true, - enableColumnFilter: true, - enableGlobalFilter: false, }), columnHelper.display({ id: "actions", - header: () => ( - - Actions - - ), + header: () => "Actions", cell: ({ row }) => (
Edit Course}> @@ -131,7 +82,8 @@ export const courseColumns = ( Edit @@ -146,7 +98,8 @@ export const courseColumns = ( Delete @@ -161,7 +114,8 @@ export const courseColumns = ( Assign TA @@ -176,7 +130,8 @@ export const courseColumns = ( Copy From a42da5e2749ad069abb3ec0617f71ae570438d48 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Fri, 21 Mar 2025 17:35:04 -0400 Subject: [PATCH 054/100] text formatting --- src/pages/Courses/CourseEditor.tsx | 10 +++++----- src/pages/Courses/CourseUtil.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/Courses/CourseEditor.tsx b/src/pages/Courses/CourseEditor.tsx index 4c932146..e849155e 100644 --- a/src/pages/Courses/CourseEditor.tsx +++ b/src/pages/Courses/CourseEditor.tsx @@ -82,7 +82,7 @@ const CourseEditor: React.FC = ({ mode }) => { useEffect(() => { if (users) { - const instructorsList: IFormOption[] = [{ label: 'Select an Instructor', value: '' }]; + const instructorsList: IFormOption[] = [{ label: 'Select an instructor', value: '' }]; // Filter by instructors by institution const onlyInstructors = users.data.filter((user: any) => @@ -236,13 +236,13 @@ useEffect(() => { /> - + @@ -257,7 +257,7 @@ useEffect(() => { type="submit" disabled={!(formik.isValid && formik.dirty) || formik.isSubmitting} > - {mode === "update" ? "Update Course" : "Create Course"} + {mode === "update" ? "Update course" : "Create course"} diff --git a/src/pages/Courses/CourseUtil.ts b/src/pages/Courses/CourseUtil.ts index c676f1b9..b9ec6830 100644 --- a/src/pages/Courses/CourseUtil.ts +++ b/src/pages/Courses/CourseUtil.ts @@ -20,7 +20,7 @@ type PermittedCourseVisibility = CourseVisibility.PRIVATE // Form options for course visibility export const courseVisibility: IFormOption[] = [ - { label: "Private Course", value: CourseVisibility.PRIVATE }, + { label: "Private course", value: CourseVisibility.PRIVATE }, ]; // Interface for course form values From 8923bf13f1982e33f584d81707c80757062492b9 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Fri, 21 Mar 2025 20:13:50 -0400 Subject: [PATCH 055/100] updated create course button --- src/pages/Courses/Course.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx index 34a907af..dfb9f94c 100644 --- a/src/pages/Courses/Course.tsx +++ b/src/pages/Courses/Course.tsx @@ -186,8 +186,8 @@ const Courses = () => {
- From 494a7ec5f2be7b8bb4a14ed15299c942274550f2 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Fri, 21 Mar 2025 20:14:17 -0400 Subject: [PATCH 056/100] updated the update course modal buttons --- src/pages/Courses/CourseEditor.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pages/Courses/CourseEditor.tsx b/src/pages/Courses/CourseEditor.tsx index e849155e..2da33f77 100644 --- a/src/pages/Courses/CourseEditor.tsx +++ b/src/pages/Courses/CourseEditor.tsx @@ -248,12 +248,8 @@ useEffect(() => { /> - - + + ), + size: 25, + enableSorting: true, + enableColumnFilter: false, + enableGlobalFilter: false, + }), + + // Team Members column: No search, no sorting + columnHelper.accessor('members', { + header: () => 'Team Members', + cell: (info) => + info.getValue().map((member) => ( +
+ + {member.name} (Student {member.id}) + +
+ )), + size: 35, + enableSorting: false, + enableColumnFilter: false, + enableGlobalFilter: false, + }), + + // Links column: No search, no sorting + columnHelper.accessor('links', { + header: () => 'Links', + cell: (info) => ( +
+ ), + size: 15, + enableSorting: false, + enableColumnFilter: false, + enableGlobalFilter: false, + }), + + // File Info column: No search, no sorting + columnHelper.accessor('fileInfo', { + header: () => 'File Info', + cell: (info) => ( +
+ {info.getValue().map((file, idx) => ( +
+
{file.name}
+
Size: {file.size}
+
Date Modified: {file.dateModified}
+
+ ))} +
+ ), + size: 25, + enableSorting: false, + enableColumnFilter: false, + enableGlobalFilter: false, + }), + + // History column: Links to history pages (No search or sorting) + columnHelper.display({ + id: 'history', + header: () => 'History', + cell: (info) => ( + History + ), + enableSorting: false, + enableColumnFilter: false, + enableGlobalFilter: false, + }), + ]; + + return columns; +}; + +export default SubmissionEntry; From 1335fe5c42ec901c5c1c199bdbb4e93c9be67b6a Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 17:23:53 -0400 Subject: [PATCH 058/100] create submission list component --- .../SubmissionTable/SubmissionList.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/pages/Submissions/SubmissionTable/SubmissionList.tsx diff --git a/src/pages/Submissions/SubmissionTable/SubmissionList.tsx b/src/pages/Submissions/SubmissionTable/SubmissionList.tsx new file mode 100644 index 00000000..f0a65f0f --- /dev/null +++ b/src/pages/Submissions/SubmissionTable/SubmissionList.tsx @@ -0,0 +1,22 @@ +import Table from "components/Table/Table"; +import { useMemo } from "react"; +import SubmissionEntry from "./SubmissionEntry"; + +const SubmissionList = ({ submissions, onGradeClick }: { submissions: any[], onGradeClick: (id: number) => void }) => { + + const columns = useMemo(() => SubmissionEntry({ onGradeClick }), [onGradeClick]); + + return ( +
+
+ + ); +}; + +export default SubmissionList; From 297b439381579f13d1596be9dc0f9a0d81f13c67 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 17:24:31 -0400 Subject: [PATCH 059/100] create submission view component --- src/pages/Submissions/SubmissionsView.tsx | 97 +++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/pages/Submissions/SubmissionsView.tsx diff --git a/src/pages/Submissions/SubmissionsView.tsx b/src/pages/Submissions/SubmissionsView.tsx new file mode 100644 index 00000000..39f8cf2d --- /dev/null +++ b/src/pages/Submissions/SubmissionsView.tsx @@ -0,0 +1,97 @@ +import { useEffect, useState } from "react"; +import { Col, Container, Form, Row } from "react-bootstrap"; +import SubmissionList from "./SubmissionTable/SubmissionList"; + +const SubmissionView = () => { + const [submissions, setSubmissions] = useState([]); + const [filteredSubmissions, setFilteredSubmissions] = useState([]); + const [assignmentFilter, setAssignmentFilter] = useState(""); + + // Dummy assignments for filtering + const assignments = ["Assignment 1", "Assignment 2", "Assignment 3"]; + + useEffect(() => { + // Simulating data fetching + const fetchSubmissions = async () => { + const data = [ + { + id: 1, + teamName: "Anonymized_Team_38121", + assignment: "Assignment 1", + members: [ + { name: "Student 10566", id: 10566 }, + { name: "Student 10559", id: 10559 }, + { name: "Student 10359", id: 10359 }, + ], + links: [ + { url: "https://github.com/example/repo", displayName: "GitHub Repository" }, + { url: "http://google.com", displayName: "Submission Link" }, + ], + fileInfo: [ + { name: "README.md", size: "14.9 KB", dateModified: "2024-10-03 23:36:57" }, + ], + }, + { + id: 2, + teamName: "Anonymized_Team_38122", + assignment: "Assignment 2", + members: [ + { name: "Student 10593", id: 10593 }, + { name: "Student 10623", id: 10623 }, + ], + links: [ + { url: "https://github.com/example/repo2", displayName: "GitHub Repository" }, + ], + fileInfo: [ + { name: "README.md", size: "11.7 KB", dateModified: "2024-10-01 12:15:00" }, + ], + }, + ]; + + setSubmissions(data); + setFilteredSubmissions(data); + }; + + fetchSubmissions(); + }, []); + + const handleGradeClick = (id: number) => { + console.log(`Assign Grade clicked for submission ID ${id}`); + }; + + const handleAssignmentChange = (e: React.ChangeEvent) => { + const selectedAssignment = e.target.value; + setAssignmentFilter(selectedAssignment); + if (selectedAssignment) { + setFilteredSubmissions(submissions.filter(sub => sub.assignment === selectedAssignment)); + } else { + setFilteredSubmissions(submissions); + } + }; + + return ( + + + +

Submissions

+
+ + Filter by Assignment + handleAssignmentChange(e as any)}> + + {assignments.map((assignment, index) => ( + + ))} + + + + +
+ + + + + ); +}; + +export default SubmissionView; From 337c35416d87832fc3fe01e4c39d6fb92185c1ea Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 17:30:02 -0400 Subject: [PATCH 060/100] add route for submission view --- src/App.tsx | 64 ++++++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 27736ba3..9230c739 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,45 +1,45 @@ -import React from "react"; +import RootLayout from "layout/Root"; +import { loadAssignment } from "pages/Assignments/AssignmentUtil"; +import AssignReviewer from "pages/Assignments/AssignReviewer"; +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 Courses from "pages/Courses/Course"; +import CourseEditor from "pages/Courses/CourseEditor"; +import { loadCourseInstructorDataAndInstitutions } from "pages/Courses/CourseUtil"; +import Questionnaire from "pages/EditQuestionnaire/Questionnaire"; +import Home from "pages/Home"; +import Participants from "pages/Participants/Participant"; +import ParticipantEditor from "pages/Participants/ParticipantEditor"; +import { loadParticipantDataRolesAndInstitutions } from "pages/Participants/participantUtil"; +import EditProfile from "pages/Profile/Edit"; +import Reviews from "pages/Reviews/reviews"; +import SubmissionsView from "pages/Submissions/SubmissionsView"; +import TA from "pages/TA/TA"; +import TAEditor from "pages/TA/TAEditor"; +import { loadTAs } from "pages/TA/TAUtil"; import { createBrowserRouter, Navigate, RouterProvider } from "react-router-dom"; import AdministratorLayout from "./layout/Administrator"; import ManageUserTypes, { loader as loadUsers } from "./pages/Administrator/ManageUserTypes"; +import Assignment from "./pages/Assignments/Assignment"; +import AssignmentEditor from "./pages/Assignments/AssignmentEditor"; import Login from "./pages/Authentication/Login"; import Logout from "./pages/Authentication/Logout"; +import Email_the_author from "./pages/Email_the_author/email_the_author"; 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 { loadParticipantDataRolesAndInstitutions } from "pages/Participants/participantUtil"; -import RootLayout from "layout/Root"; -import UserEditor from "./pages/Users/UserEditor"; import Users from "./pages/Users/User"; +import UserEditor from "./pages/Users/UserEditor"; 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 ReviewTable from "./pages/ViewTeamGrades/ReviewTable"; -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 ErrorPage from "./router/ErrorPage"; +import NotFound from "./router/NotFound"; +import ProtectedRoute from "./router/ProtectedRoute"; +import { ROLE } from "./utils/interfaces"; function App() { const router = createBrowserRouter([ { @@ -122,6 +122,10 @@ function App() { }, ], }, + { + path: "student_tasks", + element: } leastPrivilegeRole={ROLE.TA} />, + }, { path: "student_tasks/participants", element: , From 5b7c13ca271bab83948a037d2741fe803699141f Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 17:30:59 -0400 Subject: [PATCH 061/100] fix styling for container --- src/pages/ViewTeamGrades/grades.scss | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/ViewTeamGrades/grades.scss b/src/pages/ViewTeamGrades/grades.scss index e422e64f..f80b9564 100644 --- a/src/pages/ViewTeamGrades/grades.scss +++ b/src/pages/ViewTeamGrades/grades.scss @@ -231,10 +231,8 @@ .container { display: flex; - justify-content: space-between; - /* Adjust as needed */ - width: 80%; - /* Ensure the container takes up the full width */ + justify-content: space-between; /* Adjust as needed */ + width: 100%; /* Ensure the container takes up the full width */ } From 4d1bb2cbe69ebe26464588ecc65972e25dcbe4aa Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 17:45:22 -0400 Subject: [PATCH 062/100] test submission view component --- .../Submissions/SubmissionsView.test.tsx | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/pages/Submissions/SubmissionsView.test.tsx diff --git a/src/pages/Submissions/SubmissionsView.test.tsx b/src/pages/Submissions/SubmissionsView.test.tsx new file mode 100644 index 00000000..d0df159b --- /dev/null +++ b/src/pages/Submissions/SubmissionsView.test.tsx @@ -0,0 +1,54 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import SubmissionView from './SubmissionsView'; + +describe('SubmissionsView', () => { + it('renders the title and filter', () => { + render( + + + + ); + + const title = screen.getByText('Submissions'); + const filter = screen.getByLabelText('Filter by Assignment'); + + expect(title).toBeTruthy(); + expect(filter).toBeTruthy(); + }); + + it('filters submissions based on selected assignment', async () => { + render( + + + + ); + + // Select an assignment to filter + const select = screen.getByLabelText('Filter by Assignment'); + fireEvent.change(select, { target: { value: 'Assignment 1' } }); + + // Check if the filtered submission is displayed + expect(await screen.findByText('Anonymized_Team_38121')).toBeTruthy(); + expect(screen.queryByText('Anonymized_Team_38122')).toBeFalsy(); + }); + + it('shows all submissions when no filter is applied', async () => { + render( + + + + ); + + // Select an assignment to filter + const select = screen.getByLabelText('Filter by Assignment'); + fireEvent.change(select, { target: { value: 'Assignment 1' } }); + + // Reset filter + fireEvent.change(select, { target: { value: '' } }); + + // Check if all submissions are displayed + expect(await screen.findByText('Anonymized_Team_38121')).toBeTruthy(); + expect(await screen.findByText('Anonymized_Team_38122')).toBeTruthy(); + }); +}); From dc7898b6fcc37150cc38095d5cc41404a0656ed5 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 17:45:34 -0400 Subject: [PATCH 063/100] test submission list component --- .../SubmissionTable/SubmissionList.test.tsx | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/pages/Submissions/SubmissionTable/SubmissionList.test.tsx diff --git a/src/pages/Submissions/SubmissionTable/SubmissionList.test.tsx b/src/pages/Submissions/SubmissionTable/SubmissionList.test.tsx new file mode 100644 index 00000000..5f6ab171 --- /dev/null +++ b/src/pages/Submissions/SubmissionTable/SubmissionList.test.tsx @@ -0,0 +1,68 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import SubmissionList from './SubmissionList'; + +const mockSubmissions = [ + { + id: 1, + teamName: 'Team B', + assignment: 'Assignment 1', + members: [{ name: 'Student 1', id: 1 }], + links: [], + fileInfo: [], + }, + { + id: 2, + teamName: 'Team A', + assignment: 'Assignment 1', + members: [{ name: 'Student 2', id: 2 }], + links: [], + fileInfo: [], + }, +]; + +const mockOnGradeClick = jest.fn(); + +describe('SubmissionList', () => { + it('renders submission entries correctly', () => { + render( + + + + ); + + // Check if submission entry is rendered + expect(screen.getByText('Team B')).toBeTruthy(); + expect(screen.getByText('Team A')).toBeTruthy(); + }); + + it('sorts the submissions by team name', () => { + render( + + + + ); + + // Click the team name header to sort ascending + const teamNameHeader = screen.getByText('Team Name'); + fireEvent.click(teamNameHeader); + + // Get the rows that contain submission entries + const rows = screen.getAllByRole('row'); + + // Check the order of the first two submission rows (excluding the header) + expect(rows[1].innerHTML).toContain('Team A'); + expect(rows[2].innerHTML).toContain('Team B'); + + // Click again to sort descending + fireEvent.click(teamNameHeader); + + // Get the rows again after sorting + const sortedRows = screen.getAllByRole('row'); + + // Check the order of the first two submission rows (excluding the header) + expect(sortedRows[1].innerHTML).toContain('Team B'); + expect(sortedRows[2].innerHTML).toContain('Team A'); + }); + +}); From 8b132eaf50f1ce832afd2d5c8300d09b32c509d4 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 17:45:48 -0400 Subject: [PATCH 064/100] test submission entry component --- .../SubmissionTable/SubmissionEntry.test.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/pages/Submissions/SubmissionTable/SubmissionEntry.test.tsx diff --git a/src/pages/Submissions/SubmissionTable/SubmissionEntry.test.tsx b/src/pages/Submissions/SubmissionTable/SubmissionEntry.test.tsx new file mode 100644 index 00000000..41e2790b --- /dev/null +++ b/src/pages/Submissions/SubmissionTable/SubmissionEntry.test.tsx @@ -0,0 +1,43 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import SubmissionList from './SubmissionList'; + +const mockSubmissions = [ + { + id: 1, + teamName: 'Anonymized_Team_38121', + assignment: 'Assignment 1', + members: [{ name: 'Student 1', id: 1 }], + links: [], + fileInfo: [], + }, +]; + +const mockOnGradeClick = jest.fn(); + +describe('SubmissionEntry', () => { + it('displays the correct team name', () => { + render( + + + + ); + + // Check if team name is rendered correctly + expect(screen.getByText('Anonymized_Team_38121')).toBeTruthy(); + }); + + it('calls onGradeClick when the grade button is clicked', () => { + render( + + + + ); + + // Simulate the button click + const button = screen.getByRole('button', { name: /Assign Grade/i }); + fireEvent.click(button); + + expect(mockOnGradeClick).toHaveBeenCalledWith(mockSubmissions[0].id); + }); +}); From 6727ea98267f34325a85ea255df5ac7d4814f7e8 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sun, 13 Oct 2024 18:00:18 -0400 Subject: [PATCH 065/100] move existing individual submission view to submissions directory --- src/App.tsx | 4 ++-- .../SubmissionView.tsx} | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/pages/{Assignments/ViewSubmissions.tsx => Submissions/SubmissionView.tsx} (94%) diff --git a/src/App.tsx b/src/App.tsx index 9230c739..7e07e27a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,6 @@ 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 Courses from "pages/Courses/Course"; import CourseEditor from "pages/Courses/CourseEditor"; import { loadCourseInstructorDataAndInstitutions } from "pages/Courses/CourseUtil"; @@ -17,6 +16,7 @@ import { loadParticipantDataRolesAndInstitutions } from "pages/Participants/part import EditProfile from "pages/Profile/Edit"; import Reviews from "pages/Reviews/reviews"; import SubmissionsView from "pages/Submissions/SubmissionsView"; +import SubmissionView from "pages/Submissions/SubmissionView"; import TA from "pages/TA/TA"; import TAEditor from "pages/TA/TAEditor"; import { loadTAs } from "pages/TA/TAUtil"; @@ -72,7 +72,7 @@ function App() { }, { path: "assignments/edit/:id/viewsubmissions", - element: , + element: , loader: loadAssignment, }, { diff --git a/src/pages/Assignments/ViewSubmissions.tsx b/src/pages/Submissions/SubmissionView.tsx similarity index 94% rename from src/pages/Assignments/ViewSubmissions.tsx rename to src/pages/Submissions/SubmissionView.tsx index d9fd69b1..48b31f77 100644 --- a/src/pages/Assignments/ViewSubmissions.tsx +++ b/src/pages/Submissions/SubmissionView.tsx @@ -1,8 +1,8 @@ import React, { useMemo } from 'react'; -import { Button, Container, Row, Col } from 'react-bootstrap'; +import { Button, Col, Container, Row } from 'react-bootstrap'; // import { useNavigate } from 'react-router-dom'; -import Table from "components/Table/Table"; import { createColumnHelper } from "@tanstack/react-table"; +import Table from "components/Table/Table"; import { useLoaderData } from 'react-router-dom'; interface ISubmission { @@ -12,7 +12,7 @@ interface ISubmission { const columnHelper = createColumnHelper(); -const ViewSubmissions: React.FC = () => { +const SubmissionView: React.FC = () => { const assignment: any = useLoaderData(); // const navigate = useNavigate(); @@ -81,4 +81,4 @@ const ViewSubmissions: React.FC = () => { ); }; -export default ViewSubmissions; \ No newline at end of file +export default SubmissionView; \ No newline at end of file From a0d36d40d2e9d44ef116f44952f339ed332350f7 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Sat, 19 Oct 2024 13:33:48 -0400 Subject: [PATCH 066/100] update view to include 23 mocked users --- src/pages/Submissions/SubmissionView.tsx | 7 -- src/pages/Submissions/SubmissionsView.tsx | 83 +++++++++++++---------- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/pages/Submissions/SubmissionView.tsx b/src/pages/Submissions/SubmissionView.tsx index 48b31f77..4e8f14cc 100644 --- a/src/pages/Submissions/SubmissionView.tsx +++ b/src/pages/Submissions/SubmissionView.tsx @@ -70,13 +70,6 @@ const SubmissionView: React.FC = () => { /> - {/* - - - - */} ); }; diff --git a/src/pages/Submissions/SubmissionsView.tsx b/src/pages/Submissions/SubmissionsView.tsx index 39f8cf2d..1ed53ba5 100644 --- a/src/pages/Submissions/SubmissionsView.tsx +++ b/src/pages/Submissions/SubmissionsView.tsx @@ -13,40 +13,41 @@ const SubmissionView = () => { useEffect(() => { // Simulating data fetching const fetchSubmissions = async () => { - const data = [ - { - id: 1, - teamName: "Anonymized_Team_38121", - assignment: "Assignment 1", - members: [ - { name: "Student 10566", id: 10566 }, - { name: "Student 10559", id: 10559 }, - { name: "Student 10359", id: 10359 }, - ], - links: [ - { url: "https://github.com/example/repo", displayName: "GitHub Repository" }, - { url: "http://google.com", displayName: "Submission Link" }, - ], - fileInfo: [ - { name: "README.md", size: "14.9 KB", dateModified: "2024-10-03 23:36:57" }, - ], - }, - { - id: 2, - teamName: "Anonymized_Team_38122", - assignment: "Assignment 2", - members: [ - { name: "Student 10593", id: 10593 }, - { name: "Student 10623", id: 10623 }, - ], - links: [ - { url: "https://github.com/example/repo2", displayName: "GitHub Repository" }, - ], - fileInfo: [ - { name: "README.md", size: "11.7 KB", dateModified: "2024-10-01 12:15:00" }, - ], - }, - ]; + const date = new Date(Date.parse('04 Dec 2021 00:12:00 GMT')); + const data = Array.from({ length: 23 }, (_, i) => { + const id = i + 1; + const teamNumber = 38121 + i; + const assignmentNumber = (i % 5) + 1; + const studentCount = (i % 3) + 1; + const currentDate = new Date(new Date().setDate(date.getDate() + i)); + + const members = Array.from({ length: studentCount }, (_, j) => ({ + name: `Student ${10000 + i * 10 + j}`, + id: 10000 + i * 10 + j, + })); + + const links = [ + { url: `https://github.com/example/repo${id}`, displayName: "GitHub Repository" }, + { url: `http://example.com/submission${id}`, displayName: "Submission Link" }, + ]; + + const fileInfo = [ + { + name: `README.md`, + size: `${(Math.random() * 15 + 10).toFixed(1)} KB`, + dateModified: formatDate(currentDate), + }, + ]; + + return { + id, + teamName: `Anonymized_Team_${teamNumber}`, + assignment: `Assignment ${assignmentNumber}`, + members, + links, + fileInfo, + }; + }); setSubmissions(data); setFilteredSubmissions(data); @@ -55,6 +56,20 @@ const SubmissionView = () => { fetchSubmissions(); }, []); + const formatDate = (date: Date) => { + const padZero = (num: number) => String(num).padStart(2, '0'); + + const year = String(date.getFullYear()) // Last two digits of the year + const month = padZero(date.getMonth() + 1); // Months are zero-based, so we add 1 + const day = padZero(date.getDate()); + + const hours = padZero(date.getHours()); + const minutes = padZero(date.getMinutes()); + const seconds = padZero(date.getSeconds()); + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; + } + const handleGradeClick = (id: number) => { console.log(`Assign Grade clicked for submission ID ${id}`); }; From f7f4d067bf57b49a6d101a8d05784cef284ac3bf Mon Sep 17 00:00:00 2001 From: mjfeng Date: Sun, 27 Oct 2024 23:52:44 -0400 Subject: [PATCH 067/100] submission history view --- package-lock.json | 45 +++++- src/App.tsx | 5 + .../SubmissionHistoryView.test.tsx | 1 + .../Submissions/SubmissionHistoryView.tsx | 136 ++++++++++++++++++ .../SubmissionTable/SubmissionEntry.tsx | 2 +- 5 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 src/pages/Submissions/SubmissionHistoryView.test.tsx create mode 100644 src/pages/Submissions/SubmissionHistoryView.tsx diff --git a/package-lock.json b/package-lock.json index 3ef4561e..19bc6dc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@types/react-router-dom": "^5.3.3", "axios": "^1.4.0", "bootstrap": "^5.3.3", - "chart.js": "^3.7.0", + "chart.js": "^4.1.1", "formik": "^2.2.9", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", @@ -38,7 +38,7 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", - "recharts": "^2.12.3", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", @@ -3045,6 +3045,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -5760,9 +5766,16 @@ } }, "node_modules/chart.js": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz", - "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg==" + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.5.tgz", + "integrity": "sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } }, "node_modules/check-types": { "version": "11.2.3", @@ -16603,6 +16616,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/src/App.tsx b/src/App.tsx index 7e07e27a..83b6f354 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import EditProfile from "pages/Profile/Edit"; import Reviews from "pages/Reviews/reviews"; import SubmissionsView from "pages/Submissions/SubmissionsView"; import SubmissionView from "pages/Submissions/SubmissionView"; +import SubmissionHistoryView from "./pages/Submissions/SubmissionHistoryView"; import TA from "pages/TA/TA"; import TAEditor from "pages/TA/TAEditor"; import { loadTAs } from "pages/TA/TAUtil"; @@ -75,6 +76,10 @@ function App() { element: , loader: loadAssignment, }, + { + path: "submissions/history/:submissionId", + element: } leastPrivilegeRole={ROLE.TA} />, + }, { path: "assignments/edit/:id/viewscores", element: , diff --git a/src/pages/Submissions/SubmissionHistoryView.test.tsx b/src/pages/Submissions/SubmissionHistoryView.test.tsx new file mode 100644 index 00000000..5040fd93 --- /dev/null +++ b/src/pages/Submissions/SubmissionHistoryView.test.tsx @@ -0,0 +1 @@ +// WIP \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx new file mode 100644 index 00000000..4f16bdf6 --- /dev/null +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -0,0 +1,136 @@ +import { useEffect, useState } from 'react'; +import { Container } from 'react-bootstrap'; +import { useParams } from 'react-router-dom'; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table'; + +interface HistoryEntry { + teamId: number; + operation: string; + user: string; + content: string; + created: string; +} + +const SubmissionHistoryView = () => { + const [history, setHistory] = useState([]); + + // Does nothing at the moment but a real implementation would likely + // retrieve submission history data via the submission ID + const { submissionId } = useParams(); + + const columnHelper = createColumnHelper(); + + const columns = [ + columnHelper.accessor('teamId', { + header: 'Team Id', + cell: info => info.getValue(), + }), + columnHelper.accessor('operation', { + header: 'Operation', + cell: info => info.getValue(), + }), + columnHelper.accessor('user', { + header: 'User', + cell: info => info.getValue(), + }), + columnHelper.accessor('content', { + header: 'Content', + cell: info => info.getValue(), + }), + columnHelper.accessor('created', { + header: 'Created', + cell: info => info.getValue(), + }), + ]; + + // Load data, dummy data for now + useEffect(() => { + const dummyData: HistoryEntry[] = [ + { + teamId: 38121, + operation: 'Submit Hyperlink', + user: 'adgorkar', + content: 'https://github.ncsu.edu/adgorkar/CSC_ECE_517_Fall2024_Program_2', + created: '2024-09-17 22:38:09 -0400' + }, + { + teamId: 38121, + operation: 'Submit Hyperlink', + user: 'adgorkar', + content: 'http://152.7.176.240:8080/', + created: '2024-09-27 18:32:10 -0400' + }, + { + teamId: 38121, + operation: 'Submit File', + user: 'adgorkar', + content: 'README.md', + created: '2024-09-29 17:52:24 -0400' + }, + { + teamId: 38121, + operation: 'Remove File', + user: 'adgorkar', + content: 'README.md', + created: '2024-10-03 23:36:03 -0400' + }, + { + teamId: 38121, + operation: 'Submit File', + user: 'adgorkar', + content: 'README_4_.md', + created: '2024-10-03 23:36:57 -0400' + } + ]; + setHistory(dummyData); + }, [submissionId]); + + const table = useReactTable({ + data: history, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( + +
+
+ + + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ))} + +
+

Submission Record

+
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ + ); +}; + +export default SubmissionHistoryView; \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx index 296f3759..90842088 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx @@ -102,7 +102,7 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void id: 'history', header: () => 'History', cell: (info) => ( - History + History ), enableSorting: false, enableColumnFilter: false, From e9a07625f6d629d265e7bb8082195711b14b1a9e Mon Sep 17 00:00:00 2001 From: mjfeng Date: Sun, 27 Oct 2024 23:58:31 -0400 Subject: [PATCH 068/100] Revert "submission history view" This reverts commit 1eb5d7b8d677ab083a999119abc1434b812e4a5a. --- package-lock.json | 45 +----- src/App.tsx | 5 - .../SubmissionHistoryView.test.tsx | 1 - .../Submissions/SubmissionHistoryView.tsx | 136 ------------------ .../SubmissionTable/SubmissionEntry.tsx | 2 +- 5 files changed, 6 insertions(+), 183 deletions(-) delete mode 100644 src/pages/Submissions/SubmissionHistoryView.test.tsx delete mode 100644 src/pages/Submissions/SubmissionHistoryView.tsx diff --git a/package-lock.json b/package-lock.json index 19bc6dc8..3ef4561e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@types/react-router-dom": "^5.3.3", "axios": "^1.4.0", "bootstrap": "^5.3.3", - "chart.js": "^4.1.1", + "chart.js": "^3.7.0", "formik": "^2.2.9", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", @@ -38,7 +38,7 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", - "recharts": "^2.0.0", + "recharts": "^2.12.3", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", @@ -3045,12 +3045,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@kurkle/color": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", - "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", - "license": "MIT" - }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -5766,16 +5760,9 @@ } }, "node_modules/chart.js": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.5.tgz", - "integrity": "sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==", - "license": "MIT", - "dependencies": { - "@kurkle/color": "^0.3.0" - }, - "engines": { - "pnpm": ">=8" - } + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz", + "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg==" }, "node_modules/check-types": { "version": "11.2.3", @@ -16616,28 +16603,6 @@ "node": ">= 0.8" } }, - "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", - "license": "MIT AND ISC", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/src/App.tsx b/src/App.tsx index 83b6f354..7e07e27a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,7 +17,6 @@ import EditProfile from "pages/Profile/Edit"; import Reviews from "pages/Reviews/reviews"; import SubmissionsView from "pages/Submissions/SubmissionsView"; import SubmissionView from "pages/Submissions/SubmissionView"; -import SubmissionHistoryView from "./pages/Submissions/SubmissionHistoryView"; import TA from "pages/TA/TA"; import TAEditor from "pages/TA/TAEditor"; import { loadTAs } from "pages/TA/TAUtil"; @@ -76,10 +75,6 @@ function App() { element: , loader: loadAssignment, }, - { - path: "submissions/history/:submissionId", - element: } leastPrivilegeRole={ROLE.TA} />, - }, { path: "assignments/edit/:id/viewscores", element: , diff --git a/src/pages/Submissions/SubmissionHistoryView.test.tsx b/src/pages/Submissions/SubmissionHistoryView.test.tsx deleted file mode 100644 index 5040fd93..00000000 --- a/src/pages/Submissions/SubmissionHistoryView.test.tsx +++ /dev/null @@ -1 +0,0 @@ -// WIP \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx deleted file mode 100644 index 4f16bdf6..00000000 --- a/src/pages/Submissions/SubmissionHistoryView.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Container } from 'react-bootstrap'; -import { useParams } from 'react-router-dom'; -import { - createColumnHelper, - flexRender, - getCoreRowModel, - useReactTable, -} from '@tanstack/react-table'; - -interface HistoryEntry { - teamId: number; - operation: string; - user: string; - content: string; - created: string; -} - -const SubmissionHistoryView = () => { - const [history, setHistory] = useState([]); - - // Does nothing at the moment but a real implementation would likely - // retrieve submission history data via the submission ID - const { submissionId } = useParams(); - - const columnHelper = createColumnHelper(); - - const columns = [ - columnHelper.accessor('teamId', { - header: 'Team Id', - cell: info => info.getValue(), - }), - columnHelper.accessor('operation', { - header: 'Operation', - cell: info => info.getValue(), - }), - columnHelper.accessor('user', { - header: 'User', - cell: info => info.getValue(), - }), - columnHelper.accessor('content', { - header: 'Content', - cell: info => info.getValue(), - }), - columnHelper.accessor('created', { - header: 'Created', - cell: info => info.getValue(), - }), - ]; - - // Load data, dummy data for now - useEffect(() => { - const dummyData: HistoryEntry[] = [ - { - teamId: 38121, - operation: 'Submit Hyperlink', - user: 'adgorkar', - content: 'https://github.ncsu.edu/adgorkar/CSC_ECE_517_Fall2024_Program_2', - created: '2024-09-17 22:38:09 -0400' - }, - { - teamId: 38121, - operation: 'Submit Hyperlink', - user: 'adgorkar', - content: 'http://152.7.176.240:8080/', - created: '2024-09-27 18:32:10 -0400' - }, - { - teamId: 38121, - operation: 'Submit File', - user: 'adgorkar', - content: 'README.md', - created: '2024-09-29 17:52:24 -0400' - }, - { - teamId: 38121, - operation: 'Remove File', - user: 'adgorkar', - content: 'README.md', - created: '2024-10-03 23:36:03 -0400' - }, - { - teamId: 38121, - operation: 'Submit File', - user: 'adgorkar', - content: 'README_4_.md', - created: '2024-10-03 23:36:57 -0400' - } - ]; - setHistory(dummyData); - }, [submissionId]); - - const table = useReactTable({ - data: history, - columns, - getCoreRowModel: getCoreRowModel(), - }); - - return ( - -
- - - - - - {table.getHeaderGroups().map(headerGroup => ( - - {headerGroup.headers.map(header => ( - - ))} - - ))} - - - {table.getRowModel().rows.map(row => ( - - {row.getVisibleCells().map(cell => ( - - ))} - - ))} - -
-

Submission Record

-
- {flexRender(header.column.columnDef.header, header.getContext())} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
-
- ); -}; - -export default SubmissionHistoryView; \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx index 90842088..296f3759 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx @@ -102,7 +102,7 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void id: 'history', header: () => 'History', cell: (info) => ( - History + History ), enableSorting: false, enableColumnFilter: false, From eb7292bcf15a0439ceab4b42893d082277c62f94 Mon Sep 17 00:00:00 2001 From: mjfeng Date: Mon, 28 Oct 2024 00:05:04 -0400 Subject: [PATCH 069/100] woops made a mistake, changes without the updated package file --- src/App.tsx | 5 + .../SubmissionHistoryView.test.tsx.wip | 1 + .../Submissions/SubmissionHistoryView.tsx | 136 ++++++++++++++++++ .../SubmissionTable/SubmissionEntry.tsx | 2 +- 4 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 src/pages/Submissions/SubmissionHistoryView.test.tsx.wip create mode 100644 src/pages/Submissions/SubmissionHistoryView.tsx diff --git a/src/App.tsx b/src/App.tsx index 7e07e27a..83b6f354 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import EditProfile from "pages/Profile/Edit"; import Reviews from "pages/Reviews/reviews"; import SubmissionsView from "pages/Submissions/SubmissionsView"; import SubmissionView from "pages/Submissions/SubmissionView"; +import SubmissionHistoryView from "./pages/Submissions/SubmissionHistoryView"; import TA from "pages/TA/TA"; import TAEditor from "pages/TA/TAEditor"; import { loadTAs } from "pages/TA/TAUtil"; @@ -75,6 +76,10 @@ function App() { element: , loader: loadAssignment, }, + { + path: "submissions/history/:submissionId", + element: } leastPrivilegeRole={ROLE.TA} />, + }, { path: "assignments/edit/:id/viewscores", element: , diff --git a/src/pages/Submissions/SubmissionHistoryView.test.tsx.wip b/src/pages/Submissions/SubmissionHistoryView.test.tsx.wip new file mode 100644 index 00000000..5040fd93 --- /dev/null +++ b/src/pages/Submissions/SubmissionHistoryView.test.tsx.wip @@ -0,0 +1 @@ +// WIP \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx new file mode 100644 index 00000000..4f16bdf6 --- /dev/null +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -0,0 +1,136 @@ +import { useEffect, useState } from 'react'; +import { Container } from 'react-bootstrap'; +import { useParams } from 'react-router-dom'; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table'; + +interface HistoryEntry { + teamId: number; + operation: string; + user: string; + content: string; + created: string; +} + +const SubmissionHistoryView = () => { + const [history, setHistory] = useState([]); + + // Does nothing at the moment but a real implementation would likely + // retrieve submission history data via the submission ID + const { submissionId } = useParams(); + + const columnHelper = createColumnHelper(); + + const columns = [ + columnHelper.accessor('teamId', { + header: 'Team Id', + cell: info => info.getValue(), + }), + columnHelper.accessor('operation', { + header: 'Operation', + cell: info => info.getValue(), + }), + columnHelper.accessor('user', { + header: 'User', + cell: info => info.getValue(), + }), + columnHelper.accessor('content', { + header: 'Content', + cell: info => info.getValue(), + }), + columnHelper.accessor('created', { + header: 'Created', + cell: info => info.getValue(), + }), + ]; + + // Load data, dummy data for now + useEffect(() => { + const dummyData: HistoryEntry[] = [ + { + teamId: 38121, + operation: 'Submit Hyperlink', + user: 'adgorkar', + content: 'https://github.ncsu.edu/adgorkar/CSC_ECE_517_Fall2024_Program_2', + created: '2024-09-17 22:38:09 -0400' + }, + { + teamId: 38121, + operation: 'Submit Hyperlink', + user: 'adgorkar', + content: 'http://152.7.176.240:8080/', + created: '2024-09-27 18:32:10 -0400' + }, + { + teamId: 38121, + operation: 'Submit File', + user: 'adgorkar', + content: 'README.md', + created: '2024-09-29 17:52:24 -0400' + }, + { + teamId: 38121, + operation: 'Remove File', + user: 'adgorkar', + content: 'README.md', + created: '2024-10-03 23:36:03 -0400' + }, + { + teamId: 38121, + operation: 'Submit File', + user: 'adgorkar', + content: 'README_4_.md', + created: '2024-10-03 23:36:57 -0400' + } + ]; + setHistory(dummyData); + }, [submissionId]); + + const table = useReactTable({ + data: history, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( + +
+ + + + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ))} + +
+

Submission Record

+
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+
+ ); +}; + +export default SubmissionHistoryView; \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx index 296f3759..90842088 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx @@ -102,7 +102,7 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void id: 'history', header: () => 'History', cell: (info) => ( - History + History ), enableSorting: false, enableColumnFilter: false, From 34bab90ca601410b5e1cabb7b2c98f42e6e1ffeb Mon Sep 17 00:00:00 2001 From: mjfeng Date: Tue, 29 Oct 2024 15:09:50 -0400 Subject: [PATCH 070/100] submission history tests --- .../SubmissionHistoryView.test.tsx | 81 +++++++++++++++++++ .../SubmissionHistoryView.test.tsx.wip | 1 - 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/pages/Submissions/SubmissionHistoryView.test.tsx delete mode 100644 src/pages/Submissions/SubmissionHistoryView.test.tsx.wip diff --git a/src/pages/Submissions/SubmissionHistoryView.test.tsx b/src/pages/Submissions/SubmissionHistoryView.test.tsx new file mode 100644 index 00000000..11b1c55b --- /dev/null +++ b/src/pages/Submissions/SubmissionHistoryView.test.tsx @@ -0,0 +1,81 @@ +// SubmissionHistoryView.test.tsx +import '@testing-library/jest-dom'; +import { render, screen, within } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import SubmissionHistoryView from './SubmissionHistoryView'; + +// Mock useParams with different submission IDs for testing +const mockUseParams = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => mockUseParams() +})); + +const renderWithRouter = (component: React.ReactNode) => { + return render( + + {component} + + ); +}; + +describe('SubmissionHistoryView', () => { + beforeEach(() => { + // Reset the mock before each test + mockUseParams.mockReset(); + // Default mock return value + mockUseParams.mockReturnValue({ submissionId: '38121' }); + }); + + // Add this new test + test('receives correct submission ID from URL parameters', () => { + // Set up mock to return a specific submission ID + mockUseParams.mockReturnValue({ submissionId: '12345' }); + + renderWithRouter(); + + // Verify that the mock was called + expect(mockUseParams).toHaveBeenCalled(); + + // Get the value that useParams returned + const { submissionId } = mockUseParams(); + expect(submissionId).toBe('12345'); + }); + + test('renders submission record title', () => { + renderWithRouter(); + expect(screen.getByText('Submission Record')).toBeInTheDocument(); + }); + + test('renders table headers', () => { + renderWithRouter(); + expect(screen.getByText('Team Id')).toBeInTheDocument(); + expect(screen.getByText('Operation')).toBeInTheDocument(); + expect(screen.getByText('User')).toBeInTheDocument(); + expect(screen.getByText('Content')).toBeInTheDocument(); + expect(screen.getByText('Created')).toBeInTheDocument(); + }); + + test('displays dummy data correctly', () => { + renderWithRouter(); + + // Get all rows (excluding header rows) + const rows = screen.getAllByRole('row').slice(2); // Skip title and header rows + + // Test first row data + const firstRow = rows[0]; + const cells = within(firstRow).getAllByRole('cell'); + + expect(cells[0]).toHaveTextContent('38121'); + expect(cells[1]).toHaveTextContent('Submit Hyperlink'); + expect(cells[2]).toHaveTextContent('adgorkar'); + expect(cells[3]).toHaveTextContent('https://github.ncsu.edu/adgorkar/CSC_ECE_517_Fall2024_Program_2'); + expect(cells[4]).toHaveTextContent('2024-09-17 22:38:09 -0400'); + }); + + test('renders correct number of rows', () => { + renderWithRouter(); + const rows = screen.getAllByRole('row'); + expect(rows.length).toBe(7); // 5 data rows + 2 header rows + }); +}); \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionHistoryView.test.tsx.wip b/src/pages/Submissions/SubmissionHistoryView.test.tsx.wip deleted file mode 100644 index 5040fd93..00000000 --- a/src/pages/Submissions/SubmissionHistoryView.test.tsx.wip +++ /dev/null @@ -1 +0,0 @@ -// WIP \ No newline at end of file From 7d8e7a50daef1024a9c86436e853c3dcb5e5d854 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Tue, 29 Oct 2024 18:16:57 -0400 Subject: [PATCH 071/100] remove user link --- src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx index 90842088..373bdb0f 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx @@ -48,9 +48,10 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void cell: (info) => info.getValue().map((member) => (
- + {/* This can be used to link to the users profile once the profile component exists */} + {/* */} {member.name} (Student {member.id}) - + {/* */}
)), size: 35, From 379a298e18b3e1b5543a8d23d14a7d44801b0d02 Mon Sep 17 00:00:00 2001 From: mjfeng Date: Tue, 29 Oct 2024 19:31:40 -0400 Subject: [PATCH 072/100] test changes --- .../SubmissionHistoryView.test.tsx | 39 +++++++------------ .../Submissions/SubmissionHistoryView.tsx | 32 +++++++-------- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/src/pages/Submissions/SubmissionHistoryView.test.tsx b/src/pages/Submissions/SubmissionHistoryView.test.tsx index 11b1c55b..e85b8f21 100644 --- a/src/pages/Submissions/SubmissionHistoryView.test.tsx +++ b/src/pages/Submissions/SubmissionHistoryView.test.tsx @@ -1,10 +1,8 @@ -// SubmissionHistoryView.test.tsx import '@testing-library/jest-dom'; import { render, screen, within } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; import SubmissionHistoryView from './SubmissionHistoryView'; -// Mock useParams with different submission IDs for testing const mockUseParams = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -21,25 +19,17 @@ const renderWithRouter = (component: React.ReactNode) => { describe('SubmissionHistoryView', () => { beforeEach(() => { - // Reset the mock before each test mockUseParams.mockReset(); - // Default mock return value - mockUseParams.mockReturnValue({ submissionId: '38121' }); + mockUseParams.mockReturnValue({ submissionId: '1' }); }); - // Add this new test + // Check if Submission ID is correct test('receives correct submission ID from URL parameters', () => { - // Set up mock to return a specific submission ID - mockUseParams.mockReturnValue({ submissionId: '12345' }); - + mockUseParams.mockReturnValue({ submissionId: '1' }); renderWithRouter(); - - // Verify that the mock was called expect(mockUseParams).toHaveBeenCalled(); - - // Get the value that useParams returned const { submissionId } = mockUseParams(); - expect(submissionId).toBe('12345'); + expect(submissionId).toBe('1'); }); test('renders submission record title', () => { @@ -47,8 +37,10 @@ describe('SubmissionHistoryView', () => { expect(screen.getByText('Submission Record')).toBeInTheDocument(); }); + // Check if table renders properly test('renders table headers', () => { renderWithRouter(); + expect(screen.getByText('Team Id')).toBeInTheDocument(); expect(screen.getByText('Operation')).toBeInTheDocument(); expect(screen.getByText('User')).toBeInTheDocument(); @@ -56,26 +48,25 @@ describe('SubmissionHistoryView', () => { expect(screen.getByText('Created')).toBeInTheDocument(); }); - test('displays dummy data correctly', () => { + // Check if data is displayed correctly + test('displays data correctly', () => { renderWithRouter(); - // Get all rows (excluding header rows) - const rows = screen.getAllByRole('row').slice(2); // Skip title and header rows - - // Test first row data + const rows = screen.getAllByRole('row').slice(2); const firstRow = rows[0]; const cells = within(firstRow).getAllByRole('cell'); - expect(cells[0]).toHaveTextContent('38121'); + expect(cells[0]).toHaveTextContent('12345'); expect(cells[1]).toHaveTextContent('Submit Hyperlink'); - expect(cells[2]).toHaveTextContent('adgorkar'); - expect(cells[3]).toHaveTextContent('https://github.ncsu.edu/adgorkar/CSC_ECE_517_Fall2024_Program_2'); - expect(cells[4]).toHaveTextContent('2024-09-17 22:38:09 -0400'); + expect(cells[2]).toHaveTextContent('Test_User'); + expect(cells[3]).toHaveTextContent('https://github.ncsu.edu/masonhorne/reimplementation-front-end'); + expect(cells[4]).toHaveTextContent('2024-09-17 22:38:09'); }); + // Check if rows are displayed correctly test('renders correct number of rows', () => { renderWithRouter(); const rows = screen.getAllByRole('row'); - expect(rows.length).toBe(7); // 5 data rows + 2 header rows + expect(rows.length).toBe(7); }); }); \ No newline at end of file diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx index 4f16bdf6..e0695a41 100644 --- a/src/pages/Submissions/SubmissionHistoryView.tsx +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -52,39 +52,39 @@ const SubmissionHistoryView = () => { useEffect(() => { const dummyData: HistoryEntry[] = [ { - teamId: 38121, + teamId: 12345, operation: 'Submit Hyperlink', - user: 'adgorkar', - content: 'https://github.ncsu.edu/adgorkar/CSC_ECE_517_Fall2024_Program_2', - created: '2024-09-17 22:38:09 -0400' + user: 'Test_User', + content: 'https://github.ncsu.edu/masonhorne/reimplementation-front-end', + created: '2024-09-17 22:38:09' }, { - teamId: 38121, + teamId: 12345, operation: 'Submit Hyperlink', - user: 'adgorkar', + user: 'Test_User', content: 'http://152.7.176.240:8080/', - created: '2024-09-27 18:32:10 -0400' + created: '2024-09-27 18:32:10' }, { - teamId: 38121, + teamId: 12345, operation: 'Submit File', - user: 'adgorkar', + user: 'Test_User', content: 'README.md', - created: '2024-09-29 17:52:24 -0400' + created: '2024-09-29 17:52:24' }, { - teamId: 38121, + teamId: 12345, operation: 'Remove File', - user: 'adgorkar', + user: 'Test_User', content: 'README.md', - created: '2024-10-03 23:36:03 -0400' + created: '2024-10-03 23:36:03' }, { - teamId: 38121, + teamId: 12345, operation: 'Submit File', - user: 'adgorkar', + user: 'Test_User', content: 'README_4_.md', - created: '2024-10-03 23:36:57 -0400' + created: '2024-10-03 23:36:57' } ]; setHistory(dummyData); From f2682c0a7b7dbe37126c6f10868904b165c2c208 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Thu, 14 Nov 2024 15:40:43 -0500 Subject: [PATCH 073/100] update submissions and links to single column --- .../SubmissionTable/SubmissionEntry.tsx | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx index 373bdb0f..6f4b9fc8 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx @@ -59,45 +59,39 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void enableColumnFilter: false, enableGlobalFilter: false, }), - - // Links column: No search, no sorting - columnHelper.accessor('links', { + // Links and File Info column: No search, no sorting + columnHelper.accessor(row => ({ links: row.links, fileInfo: row.fileInfo }), { + id: 'links', header: () => 'Links', cell: (info) => ( +
+ {info.getValue().links.map((link, idx) => ( + + ))} +
- {info.getValue().map((link, idx) => ( - - ))} -
- ), - size: 15, - enableSorting: false, - enableColumnFilter: false, - enableGlobalFilter: false, - }), - - // File Info column: No search, no sorting - columnHelper.accessor('fileInfo', { - header: () => 'File Info', - cell: (info) => ( -
- {info.getValue().map((file, idx) => ( -
-
{file.name}
-
Size: {file.size}
-
Date Modified: {file.dateModified}
+
+
Name
+
Size
+
Date Modified
+
+ {info.getValue().fileInfo.map((file, idx) => ( +
+
{file.name}
+
{file.size}
+
{file.dateModified}
))}
+
), - size: 25, + size: 40, enableSorting: false, enableColumnFilter: false, enableGlobalFilter: false, }), - // History column: Links to history pages (No search or sorting) columnHelper.display({ id: 'history', From 5ce6496d05ee11c0032007833d386f5adf81f46a Mon Sep 17 00:00:00 2001 From: masonhorne Date: Thu, 14 Nov 2024 15:51:26 -0500 Subject: [PATCH 074/100] update submissions header to be above table --- src/pages/Submissions/SubmissionsView.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/Submissions/SubmissionsView.tsx b/src/pages/Submissions/SubmissionsView.tsx index 1ed53ba5..1f595c01 100644 --- a/src/pages/Submissions/SubmissionsView.tsx +++ b/src/pages/Submissions/SubmissionsView.tsx @@ -85,11 +85,15 @@ const SubmissionView = () => { }; return ( - + - +

Submissions


+ +
+ + Filter by Assignment handleAssignmentChange(e as any)}> From 610168fda1ad2e63ed01005bd20f1da90eff9d31 Mon Sep 17 00:00:00 2001 From: masonhorne Date: Thu, 14 Nov 2024 15:52:16 -0500 Subject: [PATCH 075/100] update submission history header --- src/pages/Submissions/SubmissionHistoryView.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx index e0695a41..f42764d2 100644 --- a/src/pages/Submissions/SubmissionHistoryView.tsx +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -1,12 +1,12 @@ -import { useEffect, useState } from 'react'; -import { Container } from 'react-bootstrap'; -import { useParams } from 'react-router-dom'; import { createColumnHelper, flexRender, getCoreRowModel, useReactTable, } from '@tanstack/react-table'; +import { useEffect, useState } from 'react'; +import { Container } from 'react-bootstrap'; +import { useParams } from 'react-router-dom'; interface HistoryEntry { teamId: number; @@ -103,7 +103,7 @@ const SubmissionHistoryView = () => { -

Submission Record

+

Submission History

{table.getHeaderGroups().map(headerGroup => ( From 32ad226acbbaf373c06c8a782d7808af84baf4e9 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Tue, 18 Mar 2025 16:30:21 -0400 Subject: [PATCH 076/100] text format changes to submissions --- src/pages/Submissions/SubmissionTable/SubmissionList.tsx | 2 +- src/pages/Submissions/SubmissionsView.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Submissions/SubmissionTable/SubmissionList.tsx b/src/pages/Submissions/SubmissionTable/SubmissionList.tsx index f0a65f0f..d8fb1ba1 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionList.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionList.tsx @@ -7,7 +7,7 @@ const SubmissionList = ({ submissions, onGradeClick }: { submissions: any[], onG const columns = useMemo(() => SubmissionEntry({ onGradeClick }), [onGradeClick]); return ( -
+
{ -

Submissions

+

Submissions


From 83d2252284f970a90ce2f6c07020e4f05d6c4abd Mon Sep 17 00:00:00 2001 From: aryansharma2k2 Date: Tue, 18 Mar 2025 16:50:09 -0400 Subject: [PATCH 077/100] Updating Submission History Table to standard table format. --- .../Submissions/SubmissionHistoryView.tsx | 79 +++++++------------ 1 file changed, 27 insertions(+), 52 deletions(-) diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx index f42764d2..d092c80a 100644 --- a/src/pages/Submissions/SubmissionHistoryView.tsx +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -1,12 +1,12 @@ import { createColumnHelper, + useReactTable, flexRender, getCoreRowModel, - useReactTable, } from '@tanstack/react-table'; import { useEffect, useState } from 'react'; -import { Container } from 'react-bootstrap'; import { useParams } from 'react-router-dom'; +import Table from '../../components/Table/Table'; // Make sure this is correctly imported interface HistoryEntry { teamId: number; @@ -19,73 +19,72 @@ interface HistoryEntry { const SubmissionHistoryView = () => { const [history, setHistory] = useState([]); - // Does nothing at the moment but a real implementation would likely - // retrieve submission history data via the submission ID + // Fetch submissionId from URL params (if applicable) const { submissionId } = useParams(); - + const columnHelper = createColumnHelper(); const columns = [ columnHelper.accessor('teamId', { header: 'Team Id', - cell: info => info.getValue(), + cell: (info) => info.getValue(), }), columnHelper.accessor('operation', { header: 'Operation', - cell: info => info.getValue(), + cell: (info) => info.getValue(), }), columnHelper.accessor('user', { header: 'User', - cell: info => info.getValue(), + cell: (info) => info.getValue(), }), columnHelper.accessor('content', { header: 'Content', - cell: info => info.getValue(), + cell: (info) => info.getValue(), }), columnHelper.accessor('created', { header: 'Created', - cell: info => info.getValue(), + cell: (info) => info.getValue(), }), ]; - // Load data, dummy data for now + // Dummy data for now useEffect(() => { const dummyData: HistoryEntry[] = [ { teamId: 12345, operation: 'Submit Hyperlink', user: 'Test_User', - content: 'https://github.ncsu.edu/masonhorne/reimplementation-front-end', - created: '2024-09-17 22:38:09' + content: 'xyz', + created: '2024-09-17 22:38:09', }, { teamId: 12345, operation: 'Submit Hyperlink', user: 'Test_User', - content: 'http://152.7.176.240:8080/', - created: '2024-09-27 18:32:10' + content: 'xyzgh', + created: '2024-09-27 18:32:10', }, { teamId: 12345, operation: 'Submit File', user: 'Test_User', content: 'README.md', - created: '2024-09-29 17:52:24' + created: '2024-09-29 17:52:24', }, { teamId: 12345, operation: 'Remove File', user: 'Test_User', content: 'README.md', - created: '2024-10-03 23:36:03' + created: '2024-10-03 23:36:03', }, { teamId: 12345, operation: 'Submit File', user: 'Test_User', content: 'README_4_.md', - created: '2024-10-03 23:36:57' - } + created: '2024-10-03 23:36:57', + }, ]; setHistory(dummyData); }, [submissionId]); @@ -97,39 +96,15 @@ const SubmissionHistoryView = () => { }); return ( - -
-
- - - - - {table.getHeaderGroups().map(headerGroup => ( - - {headerGroup.headers.map(header => ( - - ))} - - ))} - - - {table.getRowModel().rows.map(row => ( - - {row.getVisibleCells().map(cell => ( - - ))} - - ))} - -
-

Submission History

-
- {flexRender(header.column.columnDef.header, header.getContext())} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
- +
+ = 10} + /> + ); }; From 6575cc83de105d84a3ec5c15011546d47f9aa6e5 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Tue, 18 Mar 2025 17:03:02 -0400 Subject: [PATCH 078/100] fixed font size and line height --- src/pages/Submissions/SubmissionHistoryView.tsx | 2 +- src/pages/Submissions/SubmissionTable/SubmissionList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx index d092c80a..45a29c5c 100644 --- a/src/pages/Submissions/SubmissionHistoryView.tsx +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -96,7 +96,7 @@ const SubmissionHistoryView = () => { }); return ( -
+
SubmissionEntry({ onGradeClick }), [onGradeClick]); return ( -
+
Date: Tue, 18 Mar 2025 16:55:13 -0400 Subject: [PATCH 079/100] Updating path. --- src/pages/Submissions/SubmissionHistoryView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx index 45a29c5c..2451a7c9 100644 --- a/src/pages/Submissions/SubmissionHistoryView.tsx +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -6,7 +6,7 @@ import { } from '@tanstack/react-table'; import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; -import Table from '../../components/Table/Table'; // Make sure this is correctly imported +import Table from '../../components/Table/Table'; interface HistoryEntry { teamId: number; From 57df2edd5ecb080887719d0c6568c973586a90a3 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Tue, 18 Mar 2025 19:26:55 -0400 Subject: [PATCH 080/100] editedvalues for more consistency --- src/pages/Submissions/SubmissionHistoryView.tsx | 2 +- src/pages/Submissions/SubmissionTable/SubmissionList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx index 2451a7c9..a676190b 100644 --- a/src/pages/Submissions/SubmissionHistoryView.tsx +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -96,7 +96,7 @@ const SubmissionHistoryView = () => { }); return ( -
+
SubmissionEntry({ onGradeClick }), [onGradeClick]); return ( -
+
Date: Fri, 21 Mar 2025 23:45:08 -0400 Subject: [PATCH 081/100] updated pagination --- src/components/Table/Pagination.tsx | 47 ++++++++++++++----- src/components/Table/Table.tsx | 12 ++++- src/pages/Courses/Course.tsx | 9 ++-- .../Submissions/SubmissionHistoryView.tsx | 2 +- .../SubmissionTable/SubmissionList.tsx | 3 ++ 5 files changed, 53 insertions(+), 20 deletions(-) diff --git a/src/components/Table/Pagination.tsx b/src/components/Table/Pagination.tsx index e42904dc..edab49c3 100644 --- a/src/components/Table/Pagination.tsx +++ b/src/components/Table/Pagination.tsx @@ -4,16 +4,12 @@ import { Col, Pagination as BPagination, Row } from "react-bootstrap"; import Input from "../Input"; import Select from "../Select"; -/** - * @author Ankur Mundra on May, 2023 - */ - interface PaginationProps { nextPage: () => void; previousPage: () => void; canNextPage: () => boolean; canPreviousPage: () => boolean; - setPageIndex: (pageIndex: number) => void; + setPageIndex: (updater: number | ((pageIndex: number) => number)) => void; setPageSize: (pageSize: number) => void; getPageCount: () => number; getState: () => TableState; @@ -30,6 +26,28 @@ const Pagination: React.FC = (props) => { getPageCount, getState, } = props; + + const totalPages = getPageCount(); + const isPaginationDisabled = totalPages <= 1; + + const handlePageSizeChange = (e: React.ChangeEvent) => { + const selectedValue = e.target.value; + const pageSize = + selectedValue === `${Number.MAX_SAFE_INTEGER}` + ? Number.MAX_SAFE_INTEGER // Show all (effectively no pagination) + : Number(selectedValue); + + setPageSize(pageSize); + + // Reset to the first page when "Show All" is selected + if (pageSize === Number.MAX_SAFE_INTEGER) { + setPageIndex(0); + } + }; + + // Handle case when "Show All" is selected and pagination is disabled + if (isPaginationDisabled && getState().pagination.pageSize !== Number.MAX_SAFE_INTEGER) return null; + return ( @@ -38,14 +56,11 @@ const Pagination: React.FC = (props) => { previousPage()} disabled={!canPreviousPage()} /> nextPage()} disabled={!canNextPage()} /> setPageIndex(getPageCount() - 1)} + onClick={() => setPageIndex(totalPages - 1)} disabled={!canNextPage()} /> - {/**/} - {/* {`Page ${getState().pagination.pageIndex + 1} of ${getPageCount()}`}*/} - {/**/} = (props) => { input={{ type: "number", min: "1", - max: getPageCount(), + max: totalPages, defaultValue: getState().pagination.pageIndex + 1, onChange: (e: React.ChangeEvent) => { const page = e.target.value ? Number(e.target.value) - 1 : 0; @@ -69,11 +84,11 @@ const Pagination: React.FC = (props) => { { label: "Show 10", value: "10" }, { label: "Show 25", value: "25" }, { label: "Show 50", value: "50" }, + { label: "Show All", value: `${Number.MAX_SAFE_INTEGER}` }, // Add "Show All" with a large value ]} input={{ value: getState().pagination.pageSize, - onChange: (e: React.ChangeEvent) => - setPageSize(Number(e.target.value)), + onChange: handlePageSizeChange, // Call the handlePageSizeChange function }} /> @@ -81,4 +96,10 @@ const Pagination: React.FC = (props) => { ); }; -export default Pagination; +const PaginationWrapper: React.FC = (props) => { + const totalPages = props.getPageCount(); + if (totalPages <= 1 && props.getState().pagination.pageSize !== Number.MAX_SAFE_INTEGER) return null; + return ; +}; + +export default PaginationWrapper; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 8a087e72..2ab6f2e9 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -30,6 +30,7 @@ interface TableProps { tableSize?: { span: number; offset: number }; columnVisibility?: Record; onSelectionChange?: (selectedData: Record[]) => void; + disableGlobalFilter?: boolean; } const Table: React.FC = ({ @@ -41,6 +42,7 @@ const Table: React.FC = ({ onSelectionChange, columnVisibility = {}, tableSize = { span: 12, offset: 0 }, + disableGlobalFilter = false, }) => { const colsPlusSelectable = useMemo(() => { const selectableColumn: any = { @@ -154,7 +156,15 @@ const Table: React.FC = ({ )} - + { disableGlobalFilter ? + null + : ( +
+ + { isGlobalFilterVisible ? " Hide" : " Show" } +
+ ) + }
{" "} diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx index dfb9f94c..fd1ecc0b 100644 --- a/src/pages/Courses/Course.tsx +++ b/src/pages/Courses/Course.tsx @@ -3,7 +3,6 @@ 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"; @@ -13,7 +12,6 @@ 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"; /** @@ -31,6 +29,7 @@ const Courses = () => { const navigate = useNavigate(); const location = useLocation(); const dispatch = useDispatch(); + // State for course details modal const [showDetailsModal, setShowDetailsModal] = useState(false); @@ -208,8 +207,6 @@ const Courses = () => {
{ institution: auth.user.role === ROLE.SUPER_ADMIN.valueOf(), instructor: auth.user.role === ROLE.SUPER_ADMIN.valueOf(), }} - showPagination={visibleCourses.length > 10} + showGlobalFilter={false} + showColumnFilter={false} + disableGlobalFilter={true} /> diff --git a/src/pages/Submissions/SubmissionHistoryView.tsx b/src/pages/Submissions/SubmissionHistoryView.tsx index a676190b..aac40f0f 100644 --- a/src/pages/Submissions/SubmissionHistoryView.tsx +++ b/src/pages/Submissions/SubmissionHistoryView.tsx @@ -102,7 +102,7 @@ const SubmissionHistoryView = () => { columns={columns} showGlobalFilter={false} showColumnFilter={false} - showPagination={history.length >= 10} + disableGlobalFilter={true} /> ); diff --git a/src/pages/Submissions/SubmissionTable/SubmissionList.tsx b/src/pages/Submissions/SubmissionTable/SubmissionList.tsx index 207d9aca..06f89c5a 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionList.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionList.tsx @@ -14,6 +14,9 @@ const SubmissionList = ({ submissions, onGradeClick }: { submissions: any[], onG columnVisibility={{ id: false, }} + showGlobalFilter={false} + showColumnFilter={false} + disableGlobalFilter={true} /> ); From 49674bad2ca2b640e5822310f3900cc33841b372 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Fri, 21 Mar 2025 23:47:52 -0400 Subject: [PATCH 082/100] undone button changes --- src/pages/Courses/CourseColumns.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Courses/CourseColumns.tsx b/src/pages/Courses/CourseColumns.tsx index 70bdc6ae..503e0c93 100644 --- a/src/pages/Courses/CourseColumns.tsx +++ b/src/pages/Courses/CourseColumns.tsx @@ -74,7 +74,7 @@ export const courseColumns = (
Edit Course}>
- - - - {showToggleQuestion && ( - - )} - {Array.from({ length: roundData[0].reviews.length }, (_, i) => ( - - ))} - - - - - {sortedData.map((row, index) => ( - - ))} - - - {showToggleQuestion && } - {columnAverages.map((avg, index) => ( - - ))} - - -
- Question No. - - Question - {`Review ${ - i + 1 - }`} - Average - {sortOrderRow === "none" && ▲▼} - {sortOrderRow === "asc" && } - {sortOrderRow === "desc" && } -
- Avg - - {avg.toFixed(2)} -
-
-
- Average peer review score:{" "} - {averagePeerReviewScore} -
-
-
- ); - }; - - return ( -
-

Summary Report: Program 2

-
Team: {dummyData.team}
- - Team members:{" "} - {teamMembers.map((member, index) => ( - - {member} - {index !== teamMembers.length - 1 && ", "} - - ))} - - - - - -
- - - -
- - -
- - {/* Conditionally render tables based on currentRound */} - {currentRound === -1 - ? dummyDataRounds.map((roundData, index) => renderTable(roundData, index)) // Render a table for each round if "All Rounds" is selected - : renderTable(dummyDataRounds[currentRound], currentRound)} - -
- -
- -
- {showReviews && ( -
-

Reviews

- -
- )} - {ShowAuthorFeedback && ( -
-

Author Feedback

- -
- )} -
- -

-

Grade and comment for submission

- Grade: {dummyData.grade} -
- Comment: {dummyData.comment} -
- Late Penalty: {dummyData.late_penalty} -
-

- - Back -
- ); -}; - -export default ReviewTable; +import React, { useEffect, useState } from "react"; +import ReviewTableRow from "./ReviewTableRow"; +import RoundSelector from "./RoundSelector"; +import dummyDataRounds from "./Data/heatMapData.json"; +import dummyData from "./Data/dummyData.json"; +import { calculateAverages, getColorClass } from "./utils"; +import "./grades.scss"; +import { Link } from "react-router-dom"; +import Statistics from "./Statistics"; +import Filters from "./Filters"; +import ShowReviews from "./ShowReviews"; //importing show reviews component +import dummyauthorfeedback from "./Data/authorFeedback.json"; // Importing dummy data for author feedback + +const ReviewTable: React.FC = () => { + const [currentRound, setCurrentRound] = useState(-1); + const [sortOrderRow, setSortOrderRow] = useState<"asc" | "desc" | "none">("none"); + const [showToggleQuestion, setShowToggleQuestion] = useState(false); + const [open, setOpen] = useState(false); + const [teamMembers, setTeamMembers] = useState([]); + const [showReviews, setShowReviews] = useState(false); + const [ShowAuthorFeedback, setShowAuthorFeedback] = useState(false); + const [roundSelected, setRoundSelected] = useState(-1); + + useEffect(() => { + setTeamMembers(dummyData.members); + }, []); + + const toggleSortOrderRow = () => { + setSortOrderRow((prevSortOrder) => { + if (prevSortOrder === "asc") return "desc"; + if (prevSortOrder === "desc") return "none"; + return "asc"; + }); + }; + + const toggleShowReviews = () => { + setShowReviews((prev) => !prev); + }; + + const selectRound = (r: number) => { + setRoundSelected((prev) => r); + }; + + // Function to toggle the visibility of ShowAuthorFeedback component + const toggleAuthorFeedback = () => { + setShowAuthorFeedback((prev) => !prev); + }; + + const handleRoundChange = (roundIndex: number) => { + setCurrentRound(roundIndex); + }; + + const toggleShowQuestion = () => { + setShowToggleQuestion(!showToggleQuestion); + }; + + const renderTable = (roundData: any, roundIndex: number) => { + const { averagePeerReviewScore, columnAverages, sortedData } = calculateAverages( + roundData, + sortOrderRow + ); + + return ( +
+

+ Review (Round: {roundIndex + 1} of {dummyDataRounds.length}) +

+ + + + + {showToggleQuestion && ( + + )} + {Array.from({ length: roundData[0].reviews.length }, (_, i) => ( + + ))} + + + + + {sortedData.map((row, index) => ( + + ))} + + + {showToggleQuestion && } + {columnAverages.map((avg, index) => ( + + ))} + + +
+ Question No. + + Question + {`Review ${ + i + 1 + }`} + Average + {sortOrderRow === "none" && ▲▼} + {sortOrderRow === "asc" && } + {sortOrderRow === "desc" && } +
+ Avg + + {avg.toFixed(2)} +
+
+
+ Average peer review score:{" "} + {averagePeerReviewScore} +
+
+
+ ); + }; + + return ( +
+

Summary Report: Program 2

+
Team: {dummyData.team}
+ + Team members:{" "} + {teamMembers.map((member, index) => ( + + {member} + {index !== teamMembers.length - 1 && ", "} + + ))} + + + + + +
+ + + +
+ + +
+ + {/* Conditionally render tables based on currentRound */} + {currentRound === -1 + ? dummyDataRounds.map((roundData, index) => renderTable(roundData, index)) // Render a table for each round if "All Rounds" is selected + : renderTable(dummyDataRounds[currentRound], currentRound)} + +
+ +
+ +
+ {showReviews && ( +
+

Reviews

+ +
+ )} + {ShowAuthorFeedback && ( +
+

Author Feedback

+ +
+ )} +
+ +

+

Grade and comment for submission

+ Grade: {dummyData.grade} +
+ Comment: {dummyData.comment} +
+ Late Penalty: {dummyData.late_penalty} +
+

+ + Back +
+ ); +}; + +export default ReviewTable; diff --git a/src/pages/ViewTeamGrades/ReviewTableRow.tsx b/src/pages/ViewTeamGrades/ReviewTableRow.tsx index ffa4e5b6..00a49ec2 100644 --- a/src/pages/ViewTeamGrades/ReviewTableRow.tsx +++ b/src/pages/ViewTeamGrades/ReviewTableRow.tsx @@ -1,50 +1,50 @@ -import React, { useState, useEffect } from "react"; -import { getColorClass } from "./utils"; // Importing utility functions -import { ReviewData } from "./App"; // Importing the ReviewData interface from App - -// Props interface for ReviewTableRow component -interface ReviewTableRowProps { - row: ReviewData; // Data for the row - showToggleQuestion: boolean; // Flag to toggle the question column -} - -// Functional component ReviewTableRow -const ReviewTableRow: React.FC = ({ row, showToggleQuestion }) => { - return ( - - {/* Question Number */} - -
- {row.maxScore !== 1 ? ( - {row.maxScore} - ) : ( - - )} -       {row.questionNumber} -
- - {/* Toggle Question */} - {showToggleQuestion && {row.questionText}} - - {/* Review Cells */} - {row.reviews.map((review, idx) => ( - - - {review.score} - - - ))} - - {/* Row Average */} - {row.RowAvg.toFixed(2)} - - ); -}; - -export default ReviewTableRow; // Exporting the ReviewTableRow component as default +import React, { useState, useEffect } from "react"; +import { getColorClass } from "./utils"; // Importing utility functions +import { ReviewData } from "./App"; // Importing the ReviewData interface from App + +// Props interface for ReviewTableRow component +interface ReviewTableRowProps { + row: ReviewData; // Data for the row + showToggleQuestion: boolean; // Flag to toggle the question column +} + +// Functional component ReviewTableRow +const ReviewTableRow: React.FC = ({ row, showToggleQuestion }) => { + return ( + + {/* Question Number */} + +
+ {row.maxScore !== 1 ? ( + {row.maxScore} + ) : ( + + )} +       {row.questionNumber} +
+ + {/* Toggle Question */} + {showToggleQuestion && {row.questionText}} + + {/* Review Cells */} + {row.reviews.map((review, idx) => ( + + + {review.score} + + + ))} + + {/* Row Average */} + {row.RowAvg.toFixed(2)} + + ); +}; + +export default ReviewTableRow; // Exporting the ReviewTableRow component as default diff --git a/src/pages/ViewTeamGrades/RoundSelector.tsx b/src/pages/ViewTeamGrades/RoundSelector.tsx index 103362d1..c4bf06a6 100644 --- a/src/pages/ViewTeamGrades/RoundSelector.tsx +++ b/src/pages/ViewTeamGrades/RoundSelector.tsx @@ -1,38 +1,38 @@ -import React, { useState, useEffect } from "react"; -import dummyDataRounds from "./Data/heatMapData.json"; -import teamData from "./Data/dummyData.json"; - -interface RoundSelectorProps { - currentRound: number; - handleRoundChange: (roundIndex: number) => void; -} - -// RoundSelector component to display buttons for selecting rounds -const RoundSelector: React.FC = ({ currentRound, handleRoundChange }) => { - return ( -
-
- {/* Mapping over dummyDataRounds to render round buttons */} - - - {dummyDataRounds.map((round, index) => ( - - ))} - {/* Displaying team members */} -
-
- ); -}; - -export default RoundSelector; +import React, { useState, useEffect } from "react"; +import dummyDataRounds from "./Data/heatMapData.json"; +import teamData from "./Data/dummyData.json"; + +interface RoundSelectorProps { + currentRound: number; + handleRoundChange: (roundIndex: number) => void; +} + +// RoundSelector component to display buttons for selecting rounds +const RoundSelector: React.FC = ({ currentRound, handleRoundChange }) => { + return ( +
+
+ {/* Mapping over dummyDataRounds to render round buttons */} + + + {dummyDataRounds.map((round, index) => ( + + ))} + {/* Displaying team members */} +
+
+ ); +}; + +export default RoundSelector; diff --git a/src/pages/ViewTeamGrades/ShowReviews.tsx b/src/pages/ViewTeamGrades/ShowReviews.tsx index 570a412e..8a9d1b22 100644 --- a/src/pages/ViewTeamGrades/ShowReviews.tsx +++ b/src/pages/ViewTeamGrades/ShowReviews.tsx @@ -1,92 +1,92 @@ -import React from "react"; -import { getColorClass } from "./utils"; -import { RootState } from "../../store/store"; -import { useDispatch, useSelector } from "react-redux"; - -//props for the ShowReviews -interface ReviewComment { - score: number; - comment?: string; - name: string; -} - -interface Review { - questionNumber: string; - questionText: string; - reviews: ReviewComment[]; - RowAvg: number; - maxScore: number; -} - -interface ShowReviewsProps { - data: Review[][]; - roundSelected: number; -} - -//function for ShowReviews -const ShowReviews: React.FC = ({ data, roundSelected }) => { - console.log("round selected: ", roundSelected); - const rounds = data.length; - - const auth = useSelector( - (state: RootState) => state.authentication, - (prev, next) => prev.isAuthenticated === next.isAuthenticated - ); - - // Render each review for every question in each round - const renderReviews = () => { - const reviewElements: JSX.Element[] = []; - for (let r = 0; r < rounds; r++) { - if (roundSelected === 1) { - if (r == 1) { - continue; - } - } - if (roundSelected === 2) { - if (r == 0) { - continue; - } - } - const num_of_questions = data[r].length; - - // Assuming 'reviews' array exists inside the first 'question' of the first 'round'. - const num_of_reviews = data[r][0].reviews.length; - reviewElements.push(
Round {r + 1}
); - for (let i = 0; i < num_of_reviews; i++) { - if (auth.user.role !== "Student") { - reviewElements.push(
Review {i + 1}
); - } else { - reviewElements.push(
Review {i + 1}
); - } - for (let j = 0; j < num_of_questions; j++) { - reviewElements.push( -
-
- {j + 1}. {data[r][j].questionText} -
-
- - {data[r][j].reviews[i].score} - - {data[r][j].reviews[i].comment && ( -
{data[r][j].reviews[i].comment}
- )} -
-
- ); - } - } - } - - return reviewElements; - }; - - return
{rounds > 0 ? renderReviews() :
No reviews available
}
; -}; - -export default ShowReviews; +import React from "react"; +import { getColorClass } from "./utils"; +import { RootState } from "../../store/store"; +import { useDispatch, useSelector } from "react-redux"; + +//props for the ShowReviews +interface ReviewComment { + score: number; + comment?: string; + name: string; +} + +interface Review { + questionNumber: string; + questionText: string; + reviews: ReviewComment[]; + RowAvg: number; + maxScore: number; +} + +interface ShowReviewsProps { + data: Review[][]; + roundSelected: number; +} + +//function for ShowReviews +const ShowReviews: React.FC = ({ data, roundSelected }) => { + console.log("round selected: ", roundSelected); + const rounds = data.length; + + const auth = useSelector( + (state: RootState) => state.authentication, + (prev, next) => prev.isAuthenticated === next.isAuthenticated + ); + + // Render each review for every question in each round + const renderReviews = () => { + const reviewElements: JSX.Element[] = []; + for (let r = 0; r < rounds; r++) { + if (roundSelected === 1) { + if (r == 1) { + continue; + } + } + if (roundSelected === 2) { + if (r == 0) { + continue; + } + } + const num_of_questions = data[r].length; + + // Assuming 'reviews' array exists inside the first 'question' of the first 'round'. + const num_of_reviews = data[r][0].reviews.length; + reviewElements.push(
Round {r + 1}
); + for (let i = 0; i < num_of_reviews; i++) { + if (auth.user.role !== "Student") { + reviewElements.push(
Review {i + 1}
); + } else { + reviewElements.push(
Review {i + 1}
); + } + for (let j = 0; j < num_of_questions; j++) { + reviewElements.push( +
+
+ {j + 1}. {data[r][j].questionText} +
+
+ + {data[r][j].reviews[i].score} + + {data[r][j].reviews[i].comment && ( +
{data[r][j].reviews[i].comment}
+ )} +
+
+ ); + } + } + } + + return reviewElements; + }; + + return
{rounds > 0 ? renderReviews() :
No reviews available
}
; +}; + +export default ShowReviews; diff --git a/src/pages/ViewTeamGrades/Statistics.tsx b/src/pages/ViewTeamGrades/Statistics.tsx index e26d175a..19e5b557 100644 --- a/src/pages/ViewTeamGrades/Statistics.tsx +++ b/src/pages/ViewTeamGrades/Statistics.tsx @@ -1,128 +1,128 @@ -// Statistics.tsx -import React, { useState, useEffect } from "react"; -import { calculateAverages } from "./utils"; -import "./grades.scss"; -import dummyDataRounds from "./Data/heatMapData.json"; // Importing dummy data for rounds -import dummyauthorfeedback from "./Data/authorFeedback.json"; // Importing dummy data for author feedback -import teammateData from "./Data/teammateData.json"; - -//props for statistics component -interface StatisticsProps {} - -//statistics component -const Statistics: React.FC = () => { - const [sortedData, setSortedData] = useState([]); - useEffect(() => { - const { averagePeerReviewScore, columnAverages, sortedData } = calculateAverages( - dummyDataRounds[0], - "asc" - ); - const rowAvgArray = sortedData.map((item) => item.RowAvg); - console.log(rowAvgArray); - setSortedData(sortedData.map((item) => item.RowAvg)); - }, []); - - const [statisticsVisible, setstatisticsVisible] = useState(false); - const toggleStatisticsVisibility = () => { - setstatisticsVisible(!statisticsVisible); - }; - const [showReviews, setShowReviews] = useState(false); - const [ShowAuthorFeedback, setShowAuthorFeedback] = useState(false); - - const [roundSelected, setRoundSelected] = useState(-1); - - const selectRound = (r: number) => { - setRoundSelected((prev) => r); - }; - - // Function to toggle the visibility of ShowReviews component - const toggleShowReviews = () => { - setShowReviews((prev) => !prev); - }; - - // Function to toggle the visibility of ShowAuthorFeedback component - const toggleAuthorFeedback = () => { - setShowAuthorFeedback((prev) => !prev); - }; - - const headerCellStyle: React.CSSProperties = { - padding: "10px", - textAlign: "center", - }; - - //calculation for total reviews recieved - let totalReviewsForQuestion1: number = 0; - dummyDataRounds.forEach((round) => { - round.forEach((question) => { - if (question.questionNumber === "1") { - totalReviewsForQuestion1 += question.reviews.length; - } - }); - }); - //calculation for total feedback recieved - let totalfeedbackForQuestion1: number = 0; - dummyauthorfeedback.forEach((round) => { - round.forEach((question) => { - if (question.questionNumber === "1") { - totalfeedbackForQuestion1 += question.reviews.length; - } - }); - }); - - const subHeaderCellStyle: React.CSSProperties = { - padding: "10px", - textAlign: "center", - }; - - return ( -
-
Round Summary
- - - - - - - - - - - - {dummyDataRounds.map((roundData, index) => { - // Calculate averages for each category using data from utils or manually. - const submittedWorkAvg = calculateAverages(roundData, "asc").averagePeerReviewScore; - const authorFeedbackAvg = - dummyauthorfeedback[index]?.reduce((acc, item) => { - const questionScoreSum = item.reviews.reduce( - (sum, review) => sum + review.score, - 0 - ); - return acc + questionScoreSum / item.reviews.length; - }, 0) / dummyauthorfeedback[index].length; - - const teammateReviewAvg = - teammateData[index]?.reviews.reduce((acc, review) => acc + review.score, 0) / - teammateData[index]?.reviews.length; - - const finalScore = ( - (Number(submittedWorkAvg) + Number(authorFeedbackAvg) + Number(teammateReviewAvg)) / - 3 - ).toFixed(2); // Average of all three categories - - return ( - - - - - - - - ); - })} - -
RoundSubmitted Work (Avg)Author Feedback (Avg)Teammate Review (Avg)Final Score
Round {index + 1}{Number(submittedWorkAvg).toFixed(2)}{authorFeedbackAvg?.toFixed(2) || "N/A"}{teammateReviewAvg?.toFixed(2) || "N/A"}{finalScore}
-
- ); -}; - -export default Statistics; +// Statistics.tsx +import React, { useState, useEffect } from "react"; +import { calculateAverages } from "./utils"; +import "./grades.scss"; +import dummyDataRounds from "./Data/heatMapData.json"; // Importing dummy data for rounds +import dummyauthorfeedback from "./Data/authorFeedback.json"; // Importing dummy data for author feedback +import teammateData from "./Data/teammateData.json"; + +//props for statistics component +interface StatisticsProps {} + +//statistics component +const Statistics: React.FC = () => { + const [sortedData, setSortedData] = useState([]); + useEffect(() => { + const { averagePeerReviewScore, columnAverages, sortedData } = calculateAverages( + dummyDataRounds[0], + "asc" + ); + const rowAvgArray = sortedData.map((item) => item.RowAvg); + console.log(rowAvgArray); + setSortedData(sortedData.map((item) => item.RowAvg)); + }, []); + + const [statisticsVisible, setstatisticsVisible] = useState(false); + const toggleStatisticsVisibility = () => { + setstatisticsVisible(!statisticsVisible); + }; + const [showReviews, setShowReviews] = useState(false); + const [ShowAuthorFeedback, setShowAuthorFeedback] = useState(false); + + const [roundSelected, setRoundSelected] = useState(-1); + + const selectRound = (r: number) => { + setRoundSelected((prev) => r); + }; + + // Function to toggle the visibility of ShowReviews component + const toggleShowReviews = () => { + setShowReviews((prev) => !prev); + }; + + // Function to toggle the visibility of ShowAuthorFeedback component + const toggleAuthorFeedback = () => { + setShowAuthorFeedback((prev) => !prev); + }; + + const headerCellStyle: React.CSSProperties = { + padding: "10px", + textAlign: "center", + }; + + //calculation for total reviews recieved + let totalReviewsForQuestion1: number = 0; + dummyDataRounds.forEach((round) => { + round.forEach((question) => { + if (question.questionNumber === "1") { + totalReviewsForQuestion1 += question.reviews.length; + } + }); + }); + //calculation for total feedback recieved + let totalfeedbackForQuestion1: number = 0; + dummyauthorfeedback.forEach((round) => { + round.forEach((question) => { + if (question.questionNumber === "1") { + totalfeedbackForQuestion1 += question.reviews.length; + } + }); + }); + + const subHeaderCellStyle: React.CSSProperties = { + padding: "10px", + textAlign: "center", + }; + + return ( +
+
Round Summary
+ + + + + + + + + + + + {dummyDataRounds.map((roundData, index) => { + // Calculate averages for each category using data from utils or manually. + const submittedWorkAvg = calculateAverages(roundData, "asc").averagePeerReviewScore; + const authorFeedbackAvg = + dummyauthorfeedback[index]?.reduce((acc, item) => { + const questionScoreSum = item.reviews.reduce( + (sum, review) => sum + review.score, + 0 + ); + return acc + questionScoreSum / item.reviews.length; + }, 0) / dummyauthorfeedback[index].length; + + const teammateReviewAvg = + teammateData[index]?.reviews.reduce((acc, review) => acc + review.score, 0) / + teammateData[index]?.reviews.length; + + const finalScore = ( + (Number(submittedWorkAvg) + Number(authorFeedbackAvg) + Number(teammateReviewAvg)) / + 3 + ).toFixed(2); // Average of all three categories + + return ( + + + + + + + + ); + })} + +
RoundSubmitted Work (Avg)Author Feedback (Avg)Teammate Review (Avg)Final Score
Round {index + 1}{Number(submittedWorkAvg).toFixed(2)}{authorFeedbackAvg?.toFixed(2) || "N/A"}{teammateReviewAvg?.toFixed(2) || "N/A"}{finalScore}
+
+ ); +}; + +export default Statistics; diff --git a/src/pages/ViewTeamGrades/grades.scss b/src/pages/ViewTeamGrades/grades.scss index e422e64f..8598d27c 100644 --- a/src/pages/ViewTeamGrades/grades.scss +++ b/src/pages/ViewTeamGrades/grades.scss @@ -1,319 +1,319 @@ -/* Set the maximum width of the table container */ -.table-container { - max-width: 90%; -} - -/* Circle around max score number */ -.circle-container { - display: flex; - align-items: center; -} - -.circle { - width: 15px; - height: 15px; - border-radius: 50%; - border: 1px solid rgb(176, 4, 4); - display: flex; - justify-content: center; - align-items: center; - font-weight: bold; - color: rgb(176, 4, 4);; - margin-left: -20px; /* Add margin to separate the circle from the text */ -} - -.tick { - display: flex; - justify-content: center; - align-items: center; - font-weight: bold; - color: rgb(176, 4, 4);; - margin-left: -15px; /* Add margin to separate the circle from the text */ -} - -/* Colors used for coloring score cells within the heatgrid for the grades view */ - -/* Null space in the table */ -.c0 { - background-color: #d3d3d3; -} - -/* Red, indicative of a poor score */ -.c1 { - background-color: #ff8080; -} - -/* Orange */ -.c2 { - background-color: #FD992D; -} - -/* Yellow, indicative of a median score */ -.c3 { - background-color: #FFEC8B; -} - -/* Light green */ -.c4 { - background-color: #BCED91; -} - -/* Green, indicative of a good score */ -.c5 { - background-color: #2DE636; -} - -/* Default background color */ -.cf { - background-color: #FFFFFF; -} - -/* Style for the grades in the summary report */ -.grade-circle { - width: 30px; - height: 30px; - border-radius: 50%; - font-size: 15px; - color: black; - line-height: 30px; - text-align: center; -} - -/* Underline scores which have a comment */ -.underlined { - text-decoration: underline; - font-weight: bold; -} - -/* Styling for the heatgrid table */ -.tbl_heat { - border: 1px solid black; - width: 100%; - font-size: 10px; - text-align: center; - table-layout: fixed; - min-width: 600px; // Minimum width before scrolling kicks in -} - -.tbl_heat td { - cursor: pointer; - padding: 8px; - border: 1px black solid; - width: auto; - font-size: 11px; - table-layout: fixed; - position: relative; -} - -/* Tooltip display for question text on hover */ -.tbl_heat td[data-question]:hover::after { - content: attr(data-question); - position: absolute; - background-color: rgba($color: #000000, $alpha: 1); - color: #ffffff; - padding: 4px; - border-radius: 4px; - bottom: 130%; - left: 0%; - white-space: nowrap; -} - -/* Styling for table headers */ -.tbl_heat th { - border: 1px black solid; - padding: 10px; - font-size: 11px; - background-color: #f2f2f2; - width: auto; - table-layout: fixed; -} - -/* Hides padding for specific rows */ -.hiddenRow { - padding: 0 !important; -} - -/* Tooltip span styling */ -.spn_tooltip { - padding-left: 30px; - color: grey; - font-size: small; -} - -/* Styling for question toggle span */ -.spn_qsttog { - padding-left: 30px; - cursor: pointer; - text-decoration: underline; - color: blue; - font-size: small; -} - -/* Classes for E2100 Tag Reports for Students to style the new HeatGrid of review tags */ - -/* Styling for action row */ -.action_row { - border: 1px black solid; - padding: 1px 2px 2px 1px; - font-size: 11px; - text-align: center; -} - -/* Styling for tag heat grid */ -.tag_heat_grid { - padding: 0; - spacing: 0; - border: 1px solid black; - position: relative; - float: right; - top: 0px; - right: 0px; - z-index: 2; -} - -/* Styling for tag heat grid headers */ -.tag_heat_grid th { - border: 1px solid black; - font-size: 12px; - cursor: pointer; -} - -/* Styling for tag heat grid cells */ -.tag_heat_grid td { - border: 0.5px solid black; - font-size: 8px; -} - -/* Styling for tag heat grid criterion */ -.tag_heat_grid_criterion { - font-size: 11px !important; - font-weight: bold !important; -} - - -.round-selector { - display: flex; - gap: 8px; - /* Adjust spacing between buttons */ -} - -.round-button { - padding: 10px 20px; - margin: 8px; - /* Adjusts margin to remove left margin */ - border: 2px solid #b00404; - /* Red border */ - border-radius: 2px; - /* Slightly less rounded corners */ - background-color: transparent; - /* Transparent background for unselected button */ - color: #b00404; - /* Red text for unselected button */ - font-size: 14px; - font-weight: bold; - cursor: pointer; - transition: background-color 0.3s ease, color 0.3s ease; - - &:hover { - background-color: rgba(176, 4, 4, 0.1); - /* Light red background on hover */ - } - -} - -.round-button.current { - background-color: rgb(176, 4, 4); - /* Solid red background for selected button */ - color: white; - /* White text for selected button */ -} - - -.container { - display: flex; - justify-content: space-between; - /* Adjust as needed */ - width: 80%; - /* Ensure the container takes up the full width */ -} - - -.round-heading { - font-weight: bold; - margin-top: 20px; - font-size: 30px; -} - -.review-heading { - font-weight: bold; - margin-top: 10px; -} - -.review-block { - border: 1px solid #ccc; - padding: 10px; - margin-bottom: 0; -} - -.question { - font-weight: bold; -} - -.score { - border-radius: 50%; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - color: black; - margin-right: 10px; - font-weight: bold; -} - -.comment { - flex-grow: 1; - /* Ensures comment fills the rest of the container */ - padding-top: 3px; -} - -/* Style for even-numbered review blocks */ -.review-block:nth-child(even) { - background-color: #d9edf7; - -} - -/* Style for odd-numbered review blocks */ -.review-block:nth-child(odd) { - background-color: #fcf8e3; - -} - -.score-container { - display: flex; - align-items: center; - margin-top: 5px; - padding-top: 10px; - -} - - -.review-container { - margin-bottom: 200px; -} - - -.toggle-container { - margin-left: auto; - display: inline-flex; - align-items: center; - gap: 8px; -} - - -// Styling the checkbox if needed -.toggle-container input[type="checkbox"] { - margin: 0; - padding: 0; - // Reset any unwanted inherited styles +/* Set the maximum width of the table container */ +.table-container { + max-width: 90%; +} + +/* Circle around max score number */ +.circle-container { + display: flex; + align-items: center; +} + +.circle { + width: 15px; + height: 15px; + border-radius: 50%; + border: 1px solid rgb(176, 4, 4); + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + color: rgb(176, 4, 4);; + margin-left: -20px; /* Add margin to separate the circle from the text */ +} + +.tick { + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + color: rgb(176, 4, 4);; + margin-left: -15px; /* Add margin to separate the circle from the text */ +} + +/* Colors used for coloring score cells within the heatgrid for the grades view */ + +/* Null space in the table */ +.c0 { + background-color: #d3d3d3; +} + +/* Red, indicative of a poor score */ +.c1 { + background-color: #ff8080; +} + +/* Orange */ +.c2 { + background-color: #FD992D; +} + +/* Yellow, indicative of a median score */ +.c3 { + background-color: #FFEC8B; +} + +/* Light green */ +.c4 { + background-color: #BCED91; +} + +/* Green, indicative of a good score */ +.c5 { + background-color: #2DE636; +} + +/* Default background color */ +.cf { + background-color: #FFFFFF; +} + +/* Style for the grades in the summary report */ +.grade-circle { + width: 30px; + height: 30px; + border-radius: 50%; + font-size: 15px; + color: black; + line-height: 30px; + text-align: center; +} + +/* Underline scores which have a comment */ +.underlined { + text-decoration: underline; + font-weight: bold; +} + +/* Styling for the heatgrid table */ +.tbl_heat { + border: 1px solid black; + width: 100%; + font-size: 10px; + text-align: center; + table-layout: fixed; + min-width: 600px; // Minimum width before scrolling kicks in +} + +.tbl_heat td { + cursor: pointer; + padding: 8px; + border: 1px black solid; + width: auto; + font-size: 11px; + table-layout: fixed; + position: relative; +} + +/* Tooltip display for question text on hover */ +.tbl_heat td[data-question]:hover::after { + content: attr(data-question); + position: absolute; + background-color: rgba($color: #000000, $alpha: 1); + color: #ffffff; + padding: 4px; + border-radius: 4px; + bottom: 130%; + left: 0%; + white-space: nowrap; +} + +/* Styling for table headers */ +.tbl_heat th { + border: 1px black solid; + padding: 10px; + font-size: 11px; + background-color: #f2f2f2; + width: auto; + table-layout: fixed; +} + +/* Hides padding for specific rows */ +.hiddenRow { + padding: 0 !important; +} + +/* Tooltip span styling */ +.spn_tooltip { + padding-left: 30px; + color: grey; + font-size: small; +} + +/* Styling for question toggle span */ +.spn_qsttog { + padding-left: 30px; + cursor: pointer; + text-decoration: underline; + color: blue; + font-size: small; +} + +/* Classes for E2100 Tag Reports for Students to style the new HeatGrid of review tags */ + +/* Styling for action row */ +.action_row { + border: 1px black solid; + padding: 1px 2px 2px 1px; + font-size: 11px; + text-align: center; +} + +/* Styling for tag heat grid */ +.tag_heat_grid { + padding: 0; + spacing: 0; + border: 1px solid black; + position: relative; + float: right; + top: 0px; + right: 0px; + z-index: 2; +} + +/* Styling for tag heat grid headers */ +.tag_heat_grid th { + border: 1px solid black; + font-size: 12px; + cursor: pointer; +} + +/* Styling for tag heat grid cells */ +.tag_heat_grid td { + border: 0.5px solid black; + font-size: 8px; +} + +/* Styling for tag heat grid criterion */ +.tag_heat_grid_criterion { + font-size: 11px !important; + font-weight: bold !important; +} + + +.round-selector { + display: flex; + gap: 8px; + /* Adjust spacing between buttons */ +} + +.round-button { + padding: 10px 20px; + margin: 8px; + /* Adjusts margin to remove left margin */ + border: 2px solid #b00404; + /* Red border */ + border-radius: 2px; + /* Slightly less rounded corners */ + background-color: transparent; + /* Transparent background for unselected button */ + color: #b00404; + /* Red text for unselected button */ + font-size: 14px; + font-weight: bold; + cursor: pointer; + transition: background-color 0.3s ease, color 0.3s ease; + + &:hover { + background-color: rgba(176, 4, 4, 0.1); + /* Light red background on hover */ + } + +} + +.round-button.current { + background-color: rgb(176, 4, 4); + /* Solid red background for selected button */ + color: white; + /* White text for selected button */ +} + + +.container { + display: flex; + justify-content: space-between; + /* Adjust as needed */ + width: 80%; + /* Ensure the container takes up the full width */ +} + + +.round-heading { + font-weight: bold; + margin-top: 20px; + font-size: 30px; +} + +.review-heading { + font-weight: bold; + margin-top: 10px; +} + +.review-block { + border: 1px solid #ccc; + padding: 10px; + margin-bottom: 0; +} + +.question { + font-weight: bold; +} + +.score { + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + color: black; + margin-right: 10px; + font-weight: bold; +} + +.comment { + flex-grow: 1; + /* Ensures comment fills the rest of the container */ + padding-top: 3px; +} + +/* Style for even-numbered review blocks */ +.review-block:nth-child(even) { + background-color: #d9edf7; + +} + +/* Style for odd-numbered review blocks */ +.review-block:nth-child(odd) { + background-color: #fcf8e3; + +} + +.score-container { + display: flex; + align-items: center; + margin-top: 5px; + padding-top: 10px; + +} + + +.review-container { + margin-bottom: 200px; +} + + +.toggle-container { + margin-left: auto; + display: inline-flex; + align-items: center; + gap: 8px; +} + + +// Styling the checkbox if needed +.toggle-container input[type="checkbox"] { + margin: 0; + padding: 0; + // Reset any unwanted inherited styles } \ No newline at end of file From 3aff7aee75a2e17b8bee07c81f083abaefe9bc52 Mon Sep 17 00:00:00 2001 From: skalyan3 Date: Sat, 22 Mar 2025 10:27:24 -0400 Subject: [PATCH 084/100] Changed buttons and icons in Add TA to a course page according to the design guidelines --- src/pages/Courses/CourseColumns.test.tsx | 104 ----------------------- src/pages/TA/TA.tsx | 11 ++- src/pages/TA/TAEditor.tsx | 19 +++-- 3 files changed, 20 insertions(+), 114 deletions(-) delete mode 100644 src/pages/Courses/CourseColumns.test.tsx diff --git a/src/pages/Courses/CourseColumns.test.tsx b/src/pages/Courses/CourseColumns.test.tsx deleted file mode 100644 index fbe52543..00000000 --- a/src/pages/Courses/CourseColumns.test.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from "react"; -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { Row } from "@tanstack/react-table"; -import { courseColumns } from "./CourseColumns"; - -// Mock the ColumnButton component -jest.mock("../../components/ColumnButton", () => ({ id, ...props }: any) => ( - -)); - -describe("courseColumns", () => { - const mockHandleEdit = jest.fn(); - const mockHandleDelete = jest.fn(); - const mockHandleTA = jest.fn(); - const mockHandleCopy = jest.fn(); - const mockRow: Partial> = { - original: { id: "123", name: "Test Course", institution: { name: "Test Institution" } }, - }; - - test("should define all required columns", () => { - const columns = courseColumns(mockHandleEdit, mockHandleDelete, mockHandleTA, mockHandleCopy, "Super Administrator"); - expect(columns).toHaveLength(5); - - // Check each column's header - expect(columns[0].header).toBe("Name"); - expect(columns[1].header).toBe("Institution"); - expect(columns[2].header).toBe("Creation Date"); - expect(columns[3].header).toBe("Updated Date"); - expect(columns[4].header).toBe("Actions"); - }); - - test("should call handleEdit when edit button is clicked", async () => { - const actionsColumn = courseColumns( - mockHandleEdit, - mockHandleDelete, - mockHandleTA, - mockHandleCopy, - "Super Administrator" - ).find((col) => col.id === "actions"); - const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; - - render(} />); - const editButton = screen.getByTestId("edit"); - - userEvent.click(editButton); - expect(mockHandleEdit).toHaveBeenCalledTimes(1); - expect(mockHandleEdit).toHaveBeenCalledWith(mockRow); - }); - - test("should call handleDelete when delete button is clicked", async () => { - const actionsColumn = courseColumns( - mockHandleEdit, - mockHandleDelete, - mockHandleTA, - mockHandleCopy, - "Super Administrator" - ).find((col) => col.id === "actions"); - const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; - - render(} />); - const deleteButton = screen.getByTestId("delete"); - - userEvent.click(deleteButton); - expect(mockHandleDelete).toHaveBeenCalledTimes(1); - expect(mockHandleDelete).toHaveBeenCalledWith(mockRow); - }); - - test("should call handleTA when assign TA button is clicked", async () => { - const actionsColumn = courseColumns( - mockHandleEdit, - mockHandleDelete, - mockHandleTA, - mockHandleCopy, - "Super Administrator" - ).find((col) => col.id === "actions"); - const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; - - render(} />); - const assignTAButton = screen.getByTestId("assign-ta"); - - userEvent.click(assignTAButton); - expect(mockHandleTA).toHaveBeenCalledTimes(1); - expect(mockHandleTA).toHaveBeenCalledWith(mockRow); - }); - - test("should call handleCopy when copy button is clicked", async () => { - const actionsColumn = courseColumns( - mockHandleEdit, - mockHandleDelete, - mockHandleTA, - mockHandleCopy, - "Super Administrator" - ).find((col) => col.id === "actions"); - const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; - - render(} />); - const copyButton = screen.getByTestId("copy"); - - userEvent.click(copyButton); - expect(mockHandleCopy).toHaveBeenCalledTimes(1); - expect(mockHandleCopy).toHaveBeenCalledWith(mockRow); - }); -}); diff --git a/src/pages/TA/TA.tsx b/src/pages/TA/TA.tsx index f15ffcb5..60b754a3 100644 --- a/src/pages/TA/TA.tsx +++ b/src/pages/TA/TA.tsx @@ -89,12 +89,17 @@ const TAs = () => { navigate("new")} - tooltip="Add TA to this course" - icon={} + tooltip="Assign TA to this course" + icon={Edit} /> diff --git a/src/pages/TA/TAEditor.tsx b/src/pages/TA/TAEditor.tsx index 20ff8752..5d42c4bc 100644 --- a/src/pages/TA/TAEditor.tsx +++ b/src/pages/TA/TAEditor.tsx @@ -140,18 +140,23 @@ const TAEditor: React.FC = ({ mode }) => { />
- - + + ); }} From cc968d2aea5a3341eb2bd9c735b407a9395621de Mon Sep 17 00:00:00 2001 From: skalyan3 Date: Sat, 22 Mar 2025 11:33:36 -0400 Subject: [PATCH 085/100] Changed dropdown and toggle formatting --- src/pages/TA/TAEditor.tsx | 86 ++++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 20 deletions(-) diff --git a/src/pages/TA/TAEditor.tsx b/src/pages/TA/TAEditor.tsx index 5d42c4bc..0aa6af2d 100644 --- a/src/pages/TA/TAEditor.tsx +++ b/src/pages/TA/TAEditor.tsx @@ -25,6 +25,49 @@ type UserOption = { role?: string; }; +const customSelectStyles = { + control: (provided: any, state: any) => ({ + ...provided, + borderColor: state.isFocused ? '#ced4da' : '#ced4da', + boxShadow: 'none', + '&:hover': { + borderColor: '#adb5bd' + }, + fontSize: '1rem', + borderRadius: '0.25rem', + minHeight: '38px', + }), + placeholder: (provided: any) => ({ + ...provided, + color: '#6c757d', + fontSize: '0.95rem' + }), + menu: (provided: any) => ({ + ...provided, + zIndex: 9999, + border: '1px solid #ced4da', + borderRadius: '0.25rem', + boxShadow: '0 0 0.25rem rgba(0,0,0,0.05)', + }), + option: (provided: any, state: any) => ({ + ...provided, + backgroundColor: state.isSelected + ? '#d4edda' // light green for selected + : state.isFocused + ? '#f8f9fa' // light gray for hover/focus + : 'white', + color: '#212529', + fontSize: '0.95rem', + padding: '10px 12px', + cursor: 'pointer', + }), + singleValue: (provided: any) => ({ + ...provided, + color: '#212529', + fontSize: '0.95rem', + }), +}; + const initialValues: ITAFormValues = { name: "", }; @@ -123,6 +166,7 @@ const TAEditor: React.FC = ({ mode }) => { = ({ mode }) => { />
-
- From 780c2ba5a73c563fc33ba3af90d511430d4f9d56 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Sat, 22 Mar 2025 17:36:44 -0400 Subject: [PATCH 090/100] updated course copy/delete modal buttons --- src/pages/Courses/CourseCopy.tsx | 5 +---- src/pages/Courses/CourseDelete.tsx | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/pages/Courses/CourseCopy.tsx b/src/pages/Courses/CourseCopy.tsx index 17f87f20..b1719294 100644 --- a/src/pages/Courses/CourseCopy.tsx +++ b/src/pages/Courses/CourseCopy.tsx @@ -73,10 +73,7 @@ const CopyCourse: React.FC = ({ courseData, onClose }) => { - - diff --git a/src/pages/Courses/CourseDelete.tsx b/src/pages/Courses/CourseDelete.tsx index ebfa24a3..0484ded5 100644 --- a/src/pages/Courses/CourseDelete.tsx +++ b/src/pages/Courses/CourseDelete.tsx @@ -65,10 +65,7 @@ const DeleteCourse: React.FC = ({ courseData, onClose }) => {

- - From 249a6b0aae80e9e5edd9659768e0c508f56a1a65 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Sat, 22 Mar 2025 17:59:01 -0400 Subject: [PATCH 091/100] updated submissions text format --- src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx | 6 +++--- src/pages/Submissions/SubmissionsView.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx index 6f4b9fc8..1cd86a21 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx @@ -22,7 +22,7 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void onClick={column.getToggleSortingHandler()} // Toggle sorting on click style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }} > - Team Name + Team name { !column.getIsSorted() && } @@ -32,7 +32,7 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void <>
{info.getValue()}
), @@ -44,7 +44,7 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void // Team Members column: No search, no sorting columnHelper.accessor('members', { - header: () => 'Team Members', + header: () => 'Team members', cell: (info) => info.getValue().map((member) => (
diff --git a/src/pages/Submissions/SubmissionsView.tsx b/src/pages/Submissions/SubmissionsView.tsx index 94490f8e..169f03a8 100644 --- a/src/pages/Submissions/SubmissionsView.tsx +++ b/src/pages/Submissions/SubmissionsView.tsx @@ -27,8 +27,8 @@ const SubmissionView = () => { })); const links = [ - { url: `https://github.com/example/repo${id}`, displayName: "GitHub Repository" }, - { url: `http://example.com/submission${id}`, displayName: "Submission Link" }, + { url: `https://github.com/example/repo${id}`, displayName: "GitHub repository" }, + { url: `http://example.com/submission${id}`, displayName: "Submission link" }, ]; const fileInfo = [ From 82eb3f830c56b36b083adbde349439581b79ee76 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Sat, 22 Mar 2025 20:06:12 -0400 Subject: [PATCH 092/100] changed create course to create --- src/pages/Courses/Course.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx index de652b19..f62fd0e5 100644 --- a/src/pages/Courses/Course.tsx +++ b/src/pages/Courses/Course.tsx @@ -186,7 +186,7 @@ const Courses = () => { From 44ba15de9bc231340f74b335fea34092635cfd36 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Sat, 22 Mar 2025 20:12:57 -0400 Subject: [PATCH 093/100] updated manage TA table --- src/pages/TA/TA.tsx | 4 +++- src/pages/TA/TAColumns.tsx | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/TA/TA.tsx b/src/pages/TA/TA.tsx index 60b754a3..58fcdc24 100644 --- a/src/pages/TA/TA.tsx +++ b/src/pages/TA/TA.tsx @@ -112,13 +112,15 @@ const TAs = () => { ) : ( )} diff --git a/src/pages/TA/TAColumns.tsx b/src/pages/TA/TAColumns.tsx index 03b7b88f..b4b519b3 100644 --- a/src/pages/TA/TAColumns.tsx +++ b/src/pages/TA/TAColumns.tsx @@ -20,12 +20,12 @@ export const TAColumns = (handleDelete: Fn) => [ }), //create TA Name column Header columnHelper.accessor("name", { - header: "TA Name", + header: "TA name", enableSorting: true, }), //create Full TA Name column Header columnHelper.accessor("full_name", { - header: "Full Name", + header: "Full name", enableSorting: true, enableMultiSort: true, }), From 055fb408be1ff647bbedfebf252f197b67550f3b Mon Sep 17 00:00:00 2001 From: skalyan3 Date: Sun, 23 Mar 2025 12:22:29 -0400 Subject: [PATCH 094/100] Shortened hover message of TA icon --- src/pages/TA/TA.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/TA/TA.tsx b/src/pages/TA/TA.tsx index 60b754a3..c4c5a960 100644 --- a/src/pages/TA/TA.tsx +++ b/src/pages/TA/TA.tsx @@ -93,7 +93,7 @@ const TAs = () => { size="lg" className="ms-sm-2" onClick={() => navigate("new")} - tooltip="Assign TA to this course" + tooltip="Assign TA" icon={Edit Date: Sun, 23 Mar 2025 14:47:07 -0400 Subject: [PATCH 095/100] courses header and submission table format --- src/pages/Courses/Course.tsx | 4 +- .../SubmissionTable/SubmissionEntry.tsx | 76 ++++++++++++++----- src/pages/Submissions/SubmissionsView.tsx | 4 +- 3 files changed, 60 insertions(+), 24 deletions(-) diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx index f62fd0e5..508aa7bb 100644 --- a/src/pages/Courses/Course.tsx +++ b/src/pages/Courses/Course.tsx @@ -170,7 +170,7 @@ const Courses = () => { -

+

{auth.user.role === ROLE.INSTRUCTOR.valueOf() ? ( <>Instructed by: {auth.user.full_name} ) : auth.user.role === ROLE.TA.valueOf() ? ( @@ -178,7 +178,7 @@ const Courses = () => { ) : ( <>Manage Courses )} -

+ diff --git a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx index 1cd86a21..b58f539d 100644 --- a/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx +++ b/src/pages/Submissions/SubmissionTable/SubmissionEntry.tsx @@ -17,17 +17,7 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void const columns = [ // Team Name column: Sorting enabled, search disabled columnHelper.accessor('teamName', { - header: ({ column }) => ( -
- Team name - { - !column.getIsSorted() && - } -
- ), + header: () => "Team name", cell: (info) => ( <>
{info.getValue()}
@@ -64,30 +54,76 @@ const SubmissionEntry = ({ onGradeClick }: { onGradeClick: (id: number) => void id: 'links', header: () => 'Links', cell: (info) => ( -
+
{info.getValue().links.map((link, idx) => ( ))} -
+
-
Name
-
Size
-
Date Modified
+
+ Name +
+
+ Size +
+
+ Date modified +
{info.getValue().fileInfo.map((file, idx) => (
-
{file.name}
-
{file.size}
-
{file.dateModified}
+
+ {file.name} +
+
+ {file.size} +
+
+ {file.dateModified} +
))}
), - size: 40, enableSorting: false, enableColumnFilter: false, enableGlobalFilter: false, diff --git a/src/pages/Submissions/SubmissionsView.tsx b/src/pages/Submissions/SubmissionsView.tsx index 169f03a8..b591733b 100644 --- a/src/pages/Submissions/SubmissionsView.tsx +++ b/src/pages/Submissions/SubmissionsView.tsx @@ -85,7 +85,7 @@ const SubmissionView = () => { }; return ( - +

Submissions

@@ -93,7 +93,7 @@ const SubmissionView = () => { - + Filter by Assignment handleAssignmentChange(e as any)}> From fda2784bd267772ccc4cc54fe733d4d2faf87e26 Mon Sep 17 00:00:00 2001 From: aryansharma2k2 Date: Sun, 23 Mar 2025 15:27:17 -0400 Subject: [PATCH 096/100] Adding submissions to navbar. --- src/layout/Header.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/layout/Header.tsx b/src/layout/Header.tsx index 5b278dd8..155427c9 100644 --- a/src/layout/Header.tsx +++ b/src/layout/Header.tsx @@ -125,6 +125,9 @@ const Header: React.FC = () => { Courses + + Submissions + Assignments @@ -143,9 +146,6 @@ const Header: React.FC = () => { )} - - Assignments - Profile From c299341a6a92ec5e9a65d39f2b6d1a7ac70aaab0 Mon Sep 17 00:00:00 2001 From: Maya Mei Date: Sun, 23 Mar 2025 18:29:44 -0400 Subject: [PATCH 097/100] fixed add TA submission --- src/pages/TA/TAEditor.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/pages/TA/TAEditor.tsx b/src/pages/TA/TAEditor.tsx index 2a9e4d6f..ccba013c 100644 --- a/src/pages/TA/TAEditor.tsx +++ b/src/pages/TA/TAEditor.tsx @@ -113,7 +113,6 @@ const TAEditor: React.FC = ({ mode }) => { if (selectedUserData?.role === 'student') { // If selected user is a student, show confirmation modal - console.log("Student role detected...", selectedUserData); setSelectedUser(selectedUserData); setShowConfirmModal(true); } else { @@ -188,17 +187,15 @@ const TAEditor: React.FC = ({ mode }) => { /> - - + - - ); }} From 61e385b4d00df7b577b0fa595e5409ae649de909 Mon Sep 17 00:00:00 2001 From: skalyan3 Date: Mon, 24 Mar 2025 11:13:03 -0400 Subject: [PATCH 098/100] Changed TA display table delete TA icon --- src/pages/TA/TAColumns.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pages/TA/TAColumns.tsx b/src/pages/TA/TAColumns.tsx index b4b519b3..9566761c 100644 --- a/src/pages/TA/TAColumns.tsx +++ b/src/pages/TA/TAColumns.tsx @@ -41,12 +41,17 @@ export const TAColumns = (handleDelete: Fn) => [ <> handleDelete(row)} tooltip="Delete TA" - icon={} + icon={Remove TA} /> ), From a2944e3a79363c98f88cc70e0feed07e89cc7631 Mon Sep 17 00:00:00 2001 From: skalyan3 Date: Mon, 24 Mar 2025 12:04:57 -0400 Subject: [PATCH 099/100] Final changes --- src/pages/TA/TAEditor.tsx | 44 --------------------------------------- 1 file changed, 44 deletions(-) diff --git a/src/pages/TA/TAEditor.tsx b/src/pages/TA/TAEditor.tsx index ccba013c..be7507e3 100644 --- a/src/pages/TA/TAEditor.tsx +++ b/src/pages/TA/TAEditor.tsx @@ -25,49 +25,6 @@ type UserOption = { role?: string; }; -const customSelectStyles = { - control: (provided: any, state: any) => ({ - ...provided, - borderColor: state.isFocused ? '#ced4da' : '#ced4da', - boxShadow: 'none', - '&:hover': { - borderColor: '#adb5bd' - }, - fontSize: '1rem', - borderRadius: '0.25rem', - minHeight: '38px', - }), - placeholder: (provided: any) => ({ - ...provided, - color: '#6c757d', - fontSize: '0.95rem' - }), - menu: (provided: any) => ({ - ...provided, - zIndex: 9999, - border: '1px solid #ced4da', - borderRadius: '0.25rem', - boxShadow: '0 0 0.25rem rgba(0,0,0,0.05)', - }), - option: (provided: any, state: any) => ({ - ...provided, - backgroundColor: state.isSelected - ? '#d4edda' // light green for selected - : state.isFocused - ? '#f8f9fa' // light gray for hover/focus - : 'white', - color: '#212529', - fontSize: '0.95rem', - padding: '10px 12px', - cursor: 'pointer', - }), - singleValue: (provided: any) => ({ - ...provided, - color: '#212529', - fontSize: '0.95rem', - }), -}; - const initialValues: ITAFormValues = { name: "", }; @@ -169,7 +126,6 @@ const TAEditor: React.FC = ({ mode }) => {