diff --git a/package-lock.json b/package-lock.json index 3ef4561e..6916bc83 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,12 +38,13 @@ "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", "typescript": "^4.9.5", "web-vitals": "^2.1.4", + "xlsx": "^0.18.5", "yup": "^1.4.0" }, "devDependencies": { @@ -52,6 +53,7 @@ "@types/jqueryui": "^1.12.21", "@types/react-bootstrap": "^0.32.32", "@types/react-datepicker": "^4.10.0", + "@types/xlsx": "^0.0.35", "prettier": "^2.8.7" } }, @@ -3045,6 +3047,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", @@ -4292,6 +4300,13 @@ "@types/node": "*" } }, + "node_modules/@types/xlsx": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/@types/xlsx/-/xlsx-0.0.35.tgz", + "integrity": "sha512-s0x3DYHZzOkxtjqOk/Nv1ezGzpbN7I8WX+lzlV/nFfTDOv7x4d8ZwGHcnaiB8UCx89omPsftQhS5II3jeWePxQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "16.0.9", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", @@ -4762,6 +4777,15 @@ "node": ">=8.9" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -5736,6 +5760,19 @@ "node": ">=4" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5760,9 +5797,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", @@ -5939,6 +5983,15 @@ "node": ">=4" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -6160,6 +6213,18 @@ "node": ">=10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -8594,6 +8659,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -15232,6 +15306,18 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -16603,6 +16689,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", @@ -17139,6 +17247,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -17512,6 +17638,27 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml-name-validator": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", diff --git a/package.json b/package.json index a1ed9d64..36d3a24c 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "axios": "^1.4.0", "bootstrap": "^5.3.3", "chart.js": "^4.1.1", - "recharts": "^2.0.0", "formik": "^2.2.9", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", @@ -34,11 +33,13 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", "typescript": "^4.9.5", "web-vitals": "^2.1.4", + "xlsx": "^0.18.5", "yup": "^1.4.0" }, "scripts": { @@ -71,6 +72,7 @@ "@types/jqueryui": "^1.12.21", "@types/react-bootstrap": "^0.32.32", "@types/react-datepicker": "^4.10.0", + "@types/xlsx": "^0.0.35", "prettier": "^2.8.7" } } 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/components/Table/Table.tsx b/src/components/Table/Table.tsx index 8be9ca9c..0e8c6af7 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -16,6 +16,7 @@ import GlobalFilter from "./GlobalFilter"; import Pagination from "./Pagination"; import RowSelectCheckBox from "./RowSelectCheckBox"; import { FaSearch } from "react-icons/fa"; +import * as XLSX from "xlsx"; /** * @author Ankur Mundra on May, 2023 @@ -79,7 +80,8 @@ const Table: React.FC = ({ const [globalFilter, setGlobalFilter] = useState(""); const [columnFilters, setColumnFilters] = useState([]); const [columnVisibilityState, setColumnVisibilityState] = useState(columnVisibility); - const [isGlobalFilterVisible, setIsGlobalFilterVisible] = useState(showGlobalFilter); // State for global filter visibility + const [isGlobalFilterVisible, setIsGlobalFilterVisible] = useState(showGlobalFilter); + const [lastUpdated, setLastUpdated] = useState(null); const selectable = typeof onSelectionChange === "function"; const onSelectionChangeRef = useRef(onSelectionChange); @@ -118,32 +120,45 @@ const Table: React.FC = ({ getPageCount, } = table; - // Used to return early from useEffect() on mount. - const firstRenderRef = useRef(true); - // This useEffect() watches flatRows such that on change it - // calls the onSelectionChange() prop. Technically, it calls - // the onSelectionChangeRef.current function if it exists. - - const flatRows = table.getSelectedRowModel().flatRows; - useEffect(() => { - if (firstRenderRef.current) { - firstRenderRef.current = false; - return; + if (typeof onSelectionChangeRef.current === "function") { + const selectedData = table.getSelectedRowModel().flatRows.map((flatRow) => flatRow.original); + onSelectionChangeRef.current(selectedData); } + }, [table.getSelectedRowModel().flatRows]); - if (typeof onSelectionChangeRef.current !== "function") { - return; - } - const selectedData = flatRows.map((flatRow) => flatRow.original); - const handleSelectionChange = onSelectionChangeRef.current; - handleSelectionChange?.(selectedData); - }, [flatRows]); + useEffect(() => { + setLastUpdated(new Date()); + }, [initialData]); const toggleGlobalFilter = () => { setIsGlobalFilterVisible(!isGlobalFilterVisible); }; + const exportTableData = (format: "csv" | "xlsx") => { + const tableData = initialData.map((row) => { + const rowData: Record = {}; + columns.forEach((col) => { + const accessor = col.id; // Use column `id` for keys + if (accessor) { + rowData[accessor] = row[accessor]; + } + }); + return rowData; + }); + + const worksheet = XLSX.utils.json_to_sheet(tableData); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, "TableData"); + + const fileType = format === "csv" ? "csv" : "xlsx"; + XLSX.writeFile(workbook, `table_data.${fileType}`); + }; + + const refreshTableData = () => { + setLastUpdated(new Date()); + }; + return ( <> @@ -156,7 +171,7 @@ const Table: React.FC = ({ {isGlobalFilterVisible ? " Hide" : " Show"} - {" "} + @@ -166,50 +181,40 @@ const Table: React.FC = ({ {getHeaderGroups().map((headerGroup) => ( - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder ? null : ( - <> -
- {flexRender(header.column.columnDef.header, header.getContext())} - {{ - asc: " 🔼", - desc: " 🔽", - }[header.column.getIsSorted() as string] ?? null} -
- {showColumnFilter && header.column.getCanFilter() ? ( - - ) : null} - - )} - - ); - })} + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder ? null : ( +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {header.column.getIsSorted() + ? header.column.getIsSorted() === "asc" + ? " 🔼" + : " 🔽" + : null} +
+ )} + + ))} ))} - {getRowModel().rows.map((row) => { - return ( - - {row.getVisibleCells().map((cell) => { - return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ); - })} - - ); - })} + {getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} {showPagination && ( @@ -224,6 +229,22 @@ const Table: React.FC = ({ getState={getState} /> )} +
+ + +
+
+ + Last Updated: {lastUpdated ? lastUpdated.toLocaleString() : "Never"} + + +
diff --git a/src/pages/ViewTeamGrades/ReviewTable.test.tsx b/src/pages/ViewTeamGrades/ReviewTable.test.tsx new file mode 100644 index 00000000..2128f1bb --- /dev/null +++ b/src/pages/ViewTeamGrades/ReviewTable.test.tsx @@ -0,0 +1,87 @@ +import '@testing-library/jest-dom/extend-expect'; // Import jest-dom for custom assertions +import { fireEvent, render, screen } from '@testing-library/react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import ReviewTable from './ReviewTable'; + +describe('ReviewTable component', () => { + test('renders without crashing', () => { + render( + {/* Wrap your component with Router */} + + + ); + }); + +}); + + +describe('ReviewTable Toggle Menu', () => { + test('toggle menu name changes when clicked', () => { + render( + + + + ) + const menuTitleOpen = screen.getByText('â–¼ Open Heatmap Options â–¼'); + // when rendered it should ask to open + expect(menuTitleOpen).toBeInTheDocument() + + fireEvent.click(menuTitleOpen) + + const menuTitleClose = screen.getByText('â–² Close Heatmap Options â–²'); + // state will change to true, so title should ask to be closed now + expect(menuTitleClose).toBeInTheDocument() + + // closing menu should make it go back to to open + fireEvent.click(menuTitleClose) + expect(menuTitleOpen).toBeInTheDocument() + + }) + + + test('toggle menu does not show when not clicked', () => { + render( + + + + ) + + const menuTitleOpen = screen.getByText('â–¼ Open Heatmap Options â–¼') + expect(menuTitleOpen).toBeInTheDocument() + + const divForMenu = screen.queryByTestId('toggle-pannel') + expect(divForMenu).not.toBeInTheDocument() + }) + + + test('toggles each checkbox and updates the state', async () => { + render( + + + + ) + + const toggleButton = screen.getByText('â–¼ Open Heatmap Options â–¼') + fireEvent.click(toggleButton) + + // toggleQuestion checkbox + const toggleQuestion = await screen.findByLabelText('Toggle Question List') + expect(toggleQuestion).toBeInTheDocument() + fireEvent.click(toggleQuestion) + expect(toggleQuestion).toBeChecked() + + // wordCount10 checkbox + const wordCount10 = screen.getByLabelText('Toggle comments over 10 words') + expect(wordCount10).toBeInTheDocument() + fireEvent.click(wordCount10) + expect(wordCount10).toBeChecked() + + // customWordCount checkbox + const customWordCount = screen.getByLabelText('Toggle for custom feature') + expect(customWordCount).toBeInTheDocument() + fireEvent.click(customWordCount) + expect(customWordCount).toBeChecked() + }); + + +}) diff --git a/src/pages/ViewTeamGrades/ReviewTable.tsx b/src/pages/ViewTeamGrades/ReviewTable.tsx index 736c5f0c..c8b5875a 100644 --- a/src/pages/ViewTeamGrades/ReviewTable.tsx +++ b/src/pages/ViewTeamGrades/ReviewTable.tsx @@ -1,13 +1,13 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import ReviewTableRow from './ReviewTableRow'; // Importing the ReviewTableRow component import RoundSelector from './RoundSelector'; // Importing the RoundSelector component import dummyDataRounds from './Data/heatMapData.json'; // Importing dummy data for rounds import dummyData from './Data/dummyData.json'; // Importing dummy data -import { calculateAverages, getColorClass } from './utils'; // Importing utility functions +import { calculateAverages } from './utils'; // Importing utility functions import './grades.scss'; // Importing styles import { Link } from 'react-router-dom'; // Importing Link from react-router-dom import Statistics from './Statistics'; //import statistics component -import { Button, Collapse } from 'react-bootstrap'; //imporitng collaspe button +import { Collapse } from 'react-bootstrap'; //imporitng collaspe button // Functional component ReviewTable @@ -15,7 +15,19 @@ const ReviewTable: React.FC = () => { const [currentRound, setCurrentRound] = useState(0); // State for current round const [sortOrderRow, setSortOrderRow] = useState<'asc' | 'desc' | 'none'>('none'); // State for row sort order const [showToggleQuestion, setShowToggleQuestion] = useState(false); // State for showing question column - const [open, setOpen] = useState(false); + const [showToggle10WordComments, setShowToggle10WordComments] = useState(false); // State for showing > 10 word comments column + const [showToggle20WordComments, setShowToggle20WordComments] = useState(false); // State for showing > 20 word comments column + const [showSubmission, setShowSubmission] = useState(false); // State for showing user submitted links + const [showToggle, setShowToggle] = useState(false); // State for showing toggle menu panel + const [showToggleCustomWordComment, setShowToggleCustomWordComment] = useState(false); // State for showing the custom toggle column + const [inputValue, setInputValue] = useState(0); // State for holding the number input value for custom toggle + + const [teamMembers, setTeamMembers] = useState([]); + + // Fetch team members from the teamData.json file on component mount + useEffect(() => { + setTeamMembers(dummyData.members); + }, []);// Empty dependency array means it runs only once on component mount // Function to toggle the sort order for rows const toggleSortOrderRow = () => { @@ -42,26 +54,62 @@ const ReviewTable: React.FC = () => { setShowToggleQuestion(!showToggleQuestion); }; + //Function to handle > 10 word comments + const toggle10WordComments = () => { + setShowToggle10WordComments(!showToggle10WordComments); + }; + + //Function to handle > 20 word comments + const toggle20WordComments = () => { + setShowToggle20WordComments(!showToggle20WordComments); + }; + + //Function to handle custom features + const toggleCustomWordComment = () => { + // reset the input value each time custom word toggle is unchecked + if (showToggleCustomWordComment == false) { + setInputValue(0) + } + setShowToggleCustomWordComment(!showToggleCustomWordComment); + }; + + // confirm value is in bound 0-1000 + const confirmInputValue = (num: number) => { + if (num < 0 || num > 1000) { + setInputValue(0) + } else { + setInputValue(num) + } + } + // JSX rendering of the ReviewTable component return (

Summary Report: Program 2

Team: {dummyData.team}
+ {/* Displaying team members */} +
Team members: {teamMembers.map((member, index) => ( + + {member} + {index !== teamMembers.length - 1 && ', '} + + ))} +
Average peer review score:{" "} {averagePeerReviewScore}
Tagging: 97/97
- { e.preventDefault(); setOpen(!open); }}> - {open ? 'Hide Submission' : 'Show Submission'} + { e.preventDefault(); setShowSubmission(!showSubmission); }}> + {showSubmission ? 'Hide Submission' : 'Show Submission'} {/* Collapsible content */} - +


{/* Render links only when open is true */} - {open && ( + {showSubmission && ( <> {

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



- {/* toggle Question Functionality */} -
- - -
+ +
+
{e.preventDefault(); setShowToggle(!showToggle); }}> + {showToggle ? 'â–² Close Heatmap Options â–²' : 'â–¼ Open Heatmap Options â–¼'} +
+ +
+ {showToggle && ( + <> + + + {/* Allow checkbox for word count */} + + + + + + + + {showToggleCustomWordComment && ( +
+ <> + { + /* + if user deletes the input, until they type in a new value + the input will be a empty string which will be NaN + */ + let curr = event.target.value == "" ? 0 : parseInt(event.target.value) + confirmInputValue(curr) + } + } + value={inputValue} + /> +

Checking for reviews with ≥ {inputValue} characters

+ +
+ )} + + )} +
+
+
+
@@ -124,6 +229,15 @@ const ReviewTable: React.FC = () => { {sortOrderRow === "asc" && â–²} {sortOrderRow === "desc" && â–¼} + {showToggle10WordComments && ( + + )} + {showToggle20WordComments && ( + + )} + {showToggleCustomWordComment && ( + + )} @@ -132,6 +246,10 @@ const ReviewTable: React.FC = () => { key={index} row={row} showToggleQuestion={showToggleQuestion} + showToggle10WordComments={showToggle10WordComments} + showToggle20WordComments={showToggle20WordComments} + showToggleCustomWordComment={showToggleCustomWordComment} + customCharacterNumber={inputValue} /> ))} @@ -142,6 +260,9 @@ const ReviewTable: React.FC = () => { {avg.toFixed(2)} ))} + {showToggle10WordComments && } {/* Add empty cell if > 10 word toggle is clicked */} + {showToggle20WordComments && } {/* Add empty cell if > 20 word toggle is clicked */} + {showToggleCustomWordComment && } {/* Add empty cell if custom toggle is clicked */}
Comments ≥ 10 WordsComments ≥ 20 WordsComments ≥ {inputValue} characters
@@ -150,13 +271,15 @@ const ReviewTable: React.FC = () => {
{/* view stats functionality */} - -

+ +

Grade and comment for submission

- Grade: {dummyData.grade}

- Comment: {dummyData.comment}

- Late Penalty: {dummyData.late_penalty}

-

+

+ Grade: {dummyData.grade}

+ Comment: {dummyData.comment}

+ Late Penalty: {dummyData.late_penalty}

+

+
Back
diff --git a/src/pages/ViewTeamGrades/ReviewTableRow.tsx b/src/pages/ViewTeamGrades/ReviewTableRow.tsx index d9825be0..a5cb0adf 100644 --- a/src/pages/ViewTeamGrades/ReviewTableRow.tsx +++ b/src/pages/ViewTeamGrades/ReviewTableRow.tsx @@ -6,10 +6,15 @@ import { ReviewData } from './App'; // Importing the ReviewData interface from A interface ReviewTableRowProps { row: ReviewData; // Data for the row showToggleQuestion: boolean; // Flag to toggle the question column + showToggle10WordComments: boolean; // Flag to toggle the > 10 word column + showToggle20WordComments: boolean; // Flag to toggle the > 20 word column + showToggleCustomWordComment: boolean; // Flag to toggle the custom feature + customCharacterNumber: number; // } // Functional component ReviewTableRow -const ReviewTableRow: React.FC = ({ row, showToggleQuestion }) => { +const ReviewTableRow: React.FC = ({ row, showToggleQuestion, showToggle10WordComments, showToggle20WordComments, showToggleCustomWordComment, customCharacterNumber}) => { + return ( @@ -32,9 +37,9 @@ const ReviewTableRow: React.FC = ({ row, showToggleQuestion {/* Review Cells */} {row.reviews.map((review, idx) => ( {review.score} @@ -42,6 +47,22 @@ const ReviewTableRow: React.FC = ({ row, showToggleQuestion {/* Row Average */} {row.RowAvg.toFixed(2)} + + {/* Toggle >= 10 word comments */} + {showToggle10WordComments && ( + {row.reviews.filter(review => review?.comment && review.comment.split(' ').length >= 10).length} + )} + + {/* Toggle >= 20 word comments */} + {showToggle20WordComments && ( + {row.reviews.filter(review => review?.comment && review.comment.split(' ').length >= 20).length} + )} + + {/* Toggle >= (input value) character */} + {showToggleCustomWordComment && ( + {row.reviews.filter(review => review?.comment && review.comment.length >= customCharacterNumber).length} + )} + ); }; diff --git a/src/pages/ViewTeamGrades/RoundSelector.tsx b/src/pages/ViewTeamGrades/RoundSelector.tsx index fd87daf4..0edbdbb4 100644 --- a/src/pages/ViewTeamGrades/RoundSelector.tsx +++ b/src/pages/ViewTeamGrades/RoundSelector.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from 'react'; import dummyDataRounds from './Data/heatMapData.json'; -import teamData from './Data/dummyData.json'; interface RoundSelectorProps { currentRound: number; @@ -9,12 +8,6 @@ interface RoundSelectorProps { // RoundSelector component to display buttons for selecting rounds const RoundSelector: React.FC = ({ currentRound, handleRoundChange }) => { - const [teamMembers, setTeamMembers] = useState([]); - - // Fetch team members from the teamData.json file on component mount - useEffect(() => { - setTeamMembers(teamData.members); - }, []); // Empty dependency array means it runs only once on component mount return (
@@ -29,18 +22,10 @@ const RoundSelector: React.FC = ({ currentRound, handleRound Round {index + 1} ))} - {/* Displaying team members */} - - Team members: {teamMembers.map((member, index) => ( - - ({member}) - {index !== teamMembers.length - 1 && ' '} - - ))} -
); }; export default RoundSelector; + diff --git a/src/pages/ViewTeamGrades/Statistics.test.tsx b/src/pages/ViewTeamGrades/Statistics.test.tsx new file mode 100644 index 00000000..b8105b5e --- /dev/null +++ b/src/pages/ViewTeamGrades/Statistics.test.tsx @@ -0,0 +1,67 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; // Import jest-dom for custom assertions +import Statistics from './Statistics'; // Import the component to test + +describe('Statistics', () => { + test('renders the correct labels', () => { + render(); + // Find the scores within the underlined spans + const element_1 = screen.getByRole('columnheader', { name: 'Submitted Work' }); + const element_2 = screen.getByRole('columnheader', { name: 'Author Feedback' }); + const element_3 = screen.getByRole('columnheader', { name: 'Teammate Review' }); + const element_4 = screen.getByRole('columnheader', { name: 'Contributor' }); + + const element_5 = screen.getAllByText(/Average/i); + const element_6 = screen.getAllByText(/Range/i); + const element_7 = screen.getByRole('columnheader', { name: 'Final Score' }); + + // Assert that the elements are present + expect(element_1).toBeInTheDocument(); + expect(element_2).toBeInTheDocument(); + expect(element_3).toBeInTheDocument(); + expect(element_4).toBeInTheDocument(); + expect(element_5[0]).toBeInTheDocument(); + expect(element_6[0]).toBeInTheDocument(); + expect(element_7).toBeInTheDocument(); + + }); + + test('renders correct statistical information for each label', () => { + render(); + // Find the scores within the underlined spans + const element_1 = screen.getByRole('cell', { name: 'ssshah26 (Siddharth Shah)' }); + const element_2 = screen.getByRole('cell', { name: 'Show Reviews (20)' }); + const element_3 = screen.getByRole('cell', { name: '99.99% - 100%' }); + const element_4 = screen.getByRole('cell', { name: '96.67 Show Author Feedback (10)' }); + const element_5 = screen.getByRole('cell', { name: '87% - 100%' }); + const element_6 = screen.getByRole('cell', { name: '4.64' }); + const element_7 = screen.getByRole('cell', { name: '90% - 100%' }); + const element_8 = screen.getByRole('cell', { name: '75% (in Finished)' }); + + // Assert that the elements are present + expect(element_1).toBeInTheDocument(); + expect(element_2).toBeInTheDocument(); + expect(element_3).toBeInTheDocument(); + expect(element_4).toBeInTheDocument(); + expect(element_5).toBeInTheDocument(); + expect(element_6).toBeInTheDocument(); + expect(element_7).toBeInTheDocument(); + expect(element_8).toBeInTheDocument(); + }); + + test('renders correct links', () => { + render(); + // Find the scores within the underlined spans + const element_1 = screen.getByRole('link', { name: 'show stats' }); + const element_2 = screen.getByRole('link', { name: 'ssshah26' }); + const element_3 = screen.getByRole('link', { name: 'Show Reviews' }); + const element_4 = screen.getByRole('link', { name: 'Show Author Feedback' }); + + // Assert that the elements are present + expect(element_1).toBeInTheDocument(); + expect(element_2).toBeInTheDocument(); + expect(element_3).toBeInTheDocument(); + expect(element_4).toBeInTheDocument(); + + }); +}); \ No newline at end of file diff --git a/src/pages/ViewTeamGrades/Statistics.tsx b/src/pages/ViewTeamGrades/Statistics.tsx index c8c5c138..0d795606 100644 --- a/src/pages/ViewTeamGrades/Statistics.tsx +++ b/src/pages/ViewTeamGrades/Statistics.tsx @@ -77,7 +77,7 @@ const Statistics: React.FC = ({average}) => {
- +
{ e.preventDefault(); toggleStatisticsVisibility();}}> {statisticsVisible ? 'hide stats' : 'show stats'} @@ -95,58 +95,58 @@ const Statistics: React.FC = ({average}) => { )} - - + + {dummyauthorfeedback[0].length !== 0 && ( - + )} {teammateData.length !== 0 && ( - + )} - + - - - + + + {dummyauthorfeedback[0].length !== 0 && ( - + )} {dummyauthorfeedback[0].length !== 0 && ( )} {teammateData.length !== 0 && ( - + )} {teammateData.length !== 0 && ( - + )} - + - - - - - - -
Submitted WorkSubmitted WorkAuthor FeedbackAuthor FeedbackTeammate ReviewTeammate Review
ContributorAverageRangeContributorAverageRangeAverageAverageRangeAverageAverageRangeRangeFinal ScoreFinal Score
+
ssshah26 (Siddharth Shah)
-
+
+ -
+
+
99.99% - 100%
+ {dummyauthorfeedback[0].length !== 0 && ( - -
+
+
{teammateData.length !== 0 && (
)}
-
+
+
{teammateData.length !== 0 && (
90% - 100%
)}
+ {teammateData.length !== 0 && (
75%
@@ -191,7 +191,7 @@ const Statistics: React.FC = ({average}) => {
-
+
{showReviews && (

Reviews

diff --git a/src/pages/ViewTeamGrades/grades.scss b/src/pages/ViewTeamGrades/grades.scss index 8434ae5c..0651bca7 100644 --- a/src/pages/ViewTeamGrades/grades.scss +++ b/src/pages/ViewTeamGrades/grades.scss @@ -293,3 +293,20 @@ .review-container { margin-bottom: 200px; } + + +.toggle-button { + color: rgb(226, 23, 23); + display: inline; + cursor: pointer; +} +.toggle-button:hover { + text-decoration: underline; +} + +.toggle-pannel { + background-color: #f8f5f5; + width: fit-content; + padding-right: 1%; + padding-left: 1%; +} \ No newline at end of file