diff --git a/Changelog.md b/Changelog.md index e096873ab6..92ed481610 100644 --- a/Changelog.md +++ b/Changelog.md @@ -22,6 +22,7 @@ - Updated autotest seed files to ensure settings follow tester JSON schema (#7775) - Refactored grade entry form helper logic into `GradeEntryFormsController` and removed the newly-unused helper file. (#7789) - Added tests for `GradeEntryFormsController` to fully cover `update_grade_entry_form_params` (#7789) +- Updated the grade breakdown summary table to use `@tanstack/react-table` v8 (#7800) ## [v2.9.0] diff --git a/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx b/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx index f56f6eaf9a..9fbb6adb3c 100644 --- a/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx +++ b/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx @@ -1,41 +1,61 @@ import React from "react"; import {Bar} from "react-chartjs-2"; import {chartScales} from "../Helpers/chart_helpers"; -import ReactTable from "react-table"; +import Table from "../table/table"; +import {createColumnHelper} from "@tanstack/react-table"; import PropTypes from "prop-types"; import {CoreStatistics} from "./core_statistics"; import {FractionStat} from "./fraction_stat"; +const columnHelper = createColumnHelper(); + export class GradeBreakdownChart extends React.Component { render() { + const columns = [ + columnHelper.accessor("position", { + id: "position", + header: () => null, + cell: () => null, + size: 0, + enableSorting: true, + enableColumnFilter: false, + meta: { + className: "rt-hidden", + headerClassName: "rt-hidden", + }, + }), + columnHelper.accessor("name", { + header: this.props.item_name, + minSize: 150, + enableSorting: true, + enableColumnFilter: false, + }), + columnHelper.accessor("average", { + header: I18n.t("average"), + enableSorting: false, + enableColumnFilter: false, + cell: info => ( + + ), + }), + ]; let summary_table = ""; if (this.props.show_stats) { summary_table = (
- ( - - ), - }, - ]} - defaultSorted={[{id: "position"}]} - SubComponent={row => ( + columns={columns} + initialState={{ + sorting: [{id: "position"}], + columnVisibility: {position: false}, + }} + getRowCanExpand={() => true} + renderSubComponent={({row}) => (
({ + Bar: jest.fn(() =>
Bar Chart
), +})); + +jest.mock("../Assessment_Chart/fraction_stat", () => ({ + FractionStat: ({numerator, denominator}) => ( +
+ {numerator}/{denominator} +
+ ), +})); + +jest.mock("../Assessment_Chart/core_statistics", () => ({ + CoreStatistics: () =>
, +})); + +jest.mock("../table/table", () => { + return function MockTable(props) { + const firstRow = props.data[0]; + + // Execute getRowCanExpand + if (props.getRowCanExpand) { + props.getRowCanExpand(); + } + + // Execute hidden column callbacks (header & cell) + const renderedCells = props.columns.map(col => { + if (typeof col.header === "function") { + col.header(); + } + + if (typeof col.cell === "function") { + return
{col.cell({row: {original: firstRow}})}
; + } + + return null; + }); + + return ( +
+
{props.columns.length} columns
+
{props.data.length} rows
+ {renderedCells} + + {props.renderSubComponent && props.renderSubComponent({row: {original: firstRow}})} +
+ ); + }; +}); + +describe("GradeBreakdownChart when summary data exists", () => { + const defaultProps = { + show_stats: true, + summary: [ + { + name: "Quiz 1", + position: 1, + average: 85, + median: 87, + max_mark: 100, + standard_deviation: 10, + num_zeros: 2, + }, + { + name: "Quiz 2", + position: 2, + average: 90, + median: 92, + max_mark: 100, + standard_deviation: 8, + num_zeros: 1, + }, + ], + chart_title: "Grade Distribution", + distribution_data: { + labels: ["0-10", "11-20", "21-30"], + datasets: [{data: [5, 10, 15]}], + }, + item_name: "Test Quiz", + num_groupings: 50, + create_link: "/quizzes/new", + }; + + it("renders the bar chart", () => { + render(); + expect(screen.getByTestId("bar-chart")).toBeInTheDocument(); + }); + + it("renders the summary table when show_stats is true", () => { + render(); + expect(screen.getByTestId("mock-table")).toBeInTheDocument(); + }); + + it("does not render the summary table when show_stats is false", () => { + render(); + expect(screen.queryByTestId("mock-table")).not.toBeInTheDocument(); + }); + + it("passes correct number of columns to table", () => { + render(); + // 3 columns: position (hidden), name, average + expect(screen.getByTestId("table-columns")).toHaveTextContent("3 columns"); + }); + + it("passes summary data to table", () => { + render(); + expect(screen.getByTestId("table-data")).toHaveTextContent("2 rows"); + }); + + it("renders FractionStat for average column", () => { + render(); + expect(screen.getByTestId("fraction-stat")).toHaveTextContent("85/100"); + }); + + it("renders CoreStatistics when row is expanded", () => { + render(); + expect(screen.getByTestId("core-statistics")).toBeInTheDocument(); + }); + + it("sorts legend labels and tables rows by position", () => { + render(); + + const BarMock = require("react-chartjs-2").Bar; + const barProps = BarMock.mock.calls[0][0]; + const sortFn = barProps.options.plugins.legend.labels.sort; + + const a = {text: "Quiz 2"}; + const b = {text: "Quiz 1"}; + expect(sortFn(a, b)).toBeGreaterThan(0); + + const table = screen.getByTestId("mock-table"); + expect(table).toBeInTheDocument(); + expect(screen.getByTestId("table-columns")).toHaveTextContent("3 columns"); + }); +}); + +describe("GradeBreakdownChart when summary data is empty", () => { + const emptyProps = { + show_stats: true, + summary: [], + chart_title: "Grade Distribution", + distribution_data: { + labels: ["0-10", "11-20", "21-30"], + datasets: [{data: [5, 10, 15]}], + }, + item_name: "Test Quiz", + num_groupings: 50, + create_link: "/quizzes/new", + }; + it("does not render the summary table", () => { + render(); + expect(screen.queryByTestId("mock-table")).not.toBeInTheDocument(); + }); +}); diff --git a/app/javascript/Components/table/table.jsx b/app/javascript/Components/table/table.jsx index a3148da82c..730bd927ce 100644 --- a/app/javascript/Components/table/table.jsx +++ b/app/javascript/Components/table/table.jsx @@ -51,7 +51,10 @@ export default function Table({ }) { const [internalColumnFilters, setInternalColumnFilters] = React.useState([]); const [columnSizing, setColumnSizing] = React.useState({}); - const [columnVisibility, setColumnVisibility] = React.useState({inactive: false}); + const [columnVisibility, setColumnVisibility] = React.useState({ + inactive: false, + ...initialState?.columnVisibility, + }); const [expanded, setExpanded] = React.useState({}); const columnFilters = @@ -138,28 +141,30 @@ export default function Table({
))}
-
- {table.getHeaderGroups().map(headerGroup => ( -
- {headerGroup.headers.map(header => { - return ( -
- {header.column.getCanFilter() ? : null} -
- ); - })} -
- ))} -
+ {table.getAllColumns().some(column => column.getCanFilter()) && ( +
+ {table.getHeaderGroups().map(headerGroup => ( +
+ {headerGroup.headers.map(header => { + return ( +
+ {header.column.getCanFilter() ? : null} +
+ ); + })} +
+ ))} +
+ )}
{table.getRowModel().rows.map(row => { return (