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 (