From c8e2ac788d7d20e72ad80caf20a40ec39b4092bc Mon Sep 17 00:00:00 2001 From: Liz Date: Fri, 23 Jan 2026 00:38:28 -0500 Subject: [PATCH 01/10] refactor grade-breakdown-summary-table to react-table v8 --- .../grade_breakdown_chart.jsx | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx b/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx index f56f6eaf9a..26714baa30 100644 --- a/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx +++ b/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx @@ -1,41 +1,42 @@ 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"; import PropTypes from "prop-types"; import {CoreStatistics} from "./core_statistics"; import {FractionStat} from "./fraction_stat"; export class GradeBreakdownChart extends React.Component { render() { + const columns = [ + columnHelper.accessor("name", { + header: this.props.item_name, + minSize: 150, + enableSorting: true, + enableColumnFilter: true, + }), + 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} + intialState={{ + sorting: [{id: "position"}], + }} + getRowCanExpand={() => true} + renderSubComponent={({row}) => (
Date: Fri, 23 Jan 2026 00:52:44 -0500 Subject: [PATCH 02/10] removing old comments --- .../Assessment_Chart/grade_breakdown_chart.jsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx b/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx index 26714baa30..379aa2eed3 100644 --- a/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx +++ b/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx @@ -2,10 +2,13 @@ import React from "react"; import {Bar} from "react-chartjs-2"; import {chartScales} from "../Helpers/chart_helpers"; import Table from "./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 = [ @@ -20,7 +23,7 @@ export class GradeBreakdownChart extends React.Component { enableSorting: false, enableColumnFilter: false, cell: info => ( - + ), }), ]; @@ -32,8 +35,8 @@ export class GradeBreakdownChart extends React.Component { true} renderSubComponent={({row}) => ( From fb8172623f2246db71dc4276c7bdc2c192467f30 Mon Sep 17 00:00:00 2001 From: Liz Date: Fri, 23 Jan 2026 20:14:06 -0500 Subject: [PATCH 03/10] updated grade breakdown summary table to have hidden column position to allow for sorting by position --- .../grade_breakdown_chart.jsx | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx b/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx index 379aa2eed3..cfe6aa6e4b 100644 --- a/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx +++ b/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx @@ -1,7 +1,7 @@ import React from "react"; import {Bar} from "react-chartjs-2"; import {chartScales} from "../Helpers/chart_helpers"; -import Table from "./Table"; +import Table from "../table/table"; import {createColumnHelper} from "@tanstack/react-table"; import PropTypes from "prop-types"; import {CoreStatistics} from "./core_statistics"; @@ -12,18 +12,32 @@ 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, + meta: { + className: "rt-hidden", + headerClassName: "rt-hidden", + }, + }), columnHelper.accessor("name", { header: this.props.item_name, minSize: 150, enableSorting: true, - enableColumnFilter: true, + enableColumnFilter: false, }), columnHelper.accessor("average", { header: I18n.t("average"), enableSorting: false, enableColumnFilter: false, cell: info => ( - + ), }), ]; @@ -36,7 +50,8 @@ export class GradeBreakdownChart extends React.Component { data={this.props.summary} columns={columns} initialState={{ - sorting: [{id: "position", desc: false}], + sorting: [{id: "position"}], + columnVisibility: {position: false}, }} getRowCanExpand={() => true} renderSubComponent={({row}) => ( From cd6464a034950f451c0cd1b60b665f60459200da Mon Sep 17 00:00:00 2001 From: Liz Date: Fri, 23 Jan 2026 20:15:55 -0500 Subject: [PATCH 04/10] allow Table to respect initialState columnVisibility without breaking existing tables using columnVisibility --- app/javascript/Components/table/table.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/javascript/Components/table/table.jsx b/app/javascript/Components/table/table.jsx index a3148da82c..80ebb7acc0 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 = From e8479a23de0415eec0a43aba93237bfee056018c Mon Sep 17 00:00:00 2001 From: Liz Date: Fri, 23 Jan 2026 20:22:16 -0500 Subject: [PATCH 05/10] update changelog --- Changelog.md | 1 + 1 file changed, 1 insertion(+) 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] From caf4841f6481b00e9110c26aed6cd792d1d3a227 Mon Sep 17 00:00:00 2001 From: Liz Date: Sat, 24 Jan 2026 15:46:44 -0500 Subject: [PATCH 06/10] updated table to conditionally render filter row (so it doesn't appear if the table has no filters enabled) --- .../grade_breakdown_chart.jsx | 1 + .../__tests__/grade_breakdown_chart.test.jsx | 0 app/javascript/Components/table/table.jsx | 46 ++++++++++--------- 3 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx diff --git a/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx b/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx index cfe6aa6e4b..9fbb6adb3c 100644 --- a/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx +++ b/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx @@ -18,6 +18,7 @@ export class GradeBreakdownChart extends React.Component { cell: () => null, size: 0, enableSorting: true, + enableColumnFilter: false, meta: { className: "rt-hidden", headerClassName: "rt-hidden", diff --git a/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx b/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/javascript/Components/table/table.jsx b/app/javascript/Components/table/table.jsx index 80ebb7acc0..730bd927ce 100644 --- a/app/javascript/Components/table/table.jsx +++ b/app/javascript/Components/table/table.jsx @@ -141,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 ( From 5ae26c9ef5f02b71cf04d051709cd4c37894c93f Mon Sep 17 00:00:00 2001 From: Liz Date: Sat, 24 Jan 2026 15:47:04 -0500 Subject: [PATCH 07/10] added tests for grade breakdown summary table --- .../__tests__/grade_breakdown_chart.test.jsx | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx b/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx index e69de29bb2..35f0057a15 100644 --- a/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx +++ b/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx @@ -0,0 +1,97 @@ +import React from "react"; +import {render, screen} from "@testing-library/react"; +import {GradeBreakdownChart} from "../Assessment_Chart/grade_breakdown_chart"; + +jest.mock("../table/table", () => { + return function MockTable(props) { + return ( +
+
{props.columns.length} columns
+
{props.data.length} rows
+
+ ); + }; +}); + +jest.mock("react-chartjs-2", () => ({ + Bar: () =>
Bar Chart
, +})); + +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"); + }); +}); + +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(); + }); +}); From 3347219e00409421214d9c27d1dc7c1502894673 Mon Sep 17 00:00:00 2001 From: Liz Date: Sat, 24 Jan 2026 17:15:18 -0500 Subject: [PATCH 08/10] added some more tests --- .../__tests__/grade_breakdown_chart.test.jsx | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx b/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx index 35f0057a15..f7dddaf975 100644 --- a/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx +++ b/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx @@ -2,21 +2,43 @@ import React from "react"; import {render, screen} from "@testing-library/react"; import {GradeBreakdownChart} from "../Assessment_Chart/grade_breakdown_chart"; +jest.mock("react-chartjs-2", () => ({ + Bar: () =>
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]; + const averageColumn = props.columns.find( + // find the "average" column + col => col.id === "average" || col.accessorKey === "average" + ); + return (
{props.columns.length} columns
{props.data.length} rows
+ {averageColumn?.cell?.({ + row: {original: firstRow}, + })} + {props.renderSubComponent && props.renderSubComponent({row: {original: firstRow}})}
); }; }); -jest.mock("react-chartjs-2", () => ({ - Bar: () =>
Bar Chart
, -})); - describe("GradeBreakdownChart when summary data exists", () => { const defaultProps = { show_stats: true, @@ -75,6 +97,16 @@ describe("GradeBreakdownChart when summary data exists", () => { 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(); + }); }); describe("GradeBreakdownChart when summary data is empty", () => { From 78ba08ac3185650ec7b16a8f974b146802d32815 Mon Sep 17 00:00:00 2001 From: Liz Date: Sat, 24 Jan 2026 21:32:24 -0500 Subject: [PATCH 09/10] adding some tests to ensure graph legend and table sorted by position --- .../__tests__/grade_breakdown_chart.test.jsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx b/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx index f7dddaf975..d8f9e87d6c 100644 --- a/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx +++ b/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx @@ -3,7 +3,7 @@ import {render, screen} from "@testing-library/react"; import {GradeBreakdownChart} from "../Assessment_Chart/grade_breakdown_chart"; jest.mock("react-chartjs-2", () => ({ - Bar: () =>
Bar Chart
, + Bar: jest.fn(() =>
Bar Chart
), })); jest.mock("../Assessment_Chart/fraction_stat", () => ({ @@ -107,6 +107,22 @@ describe("GradeBreakdownChart when summary data exists", () => { 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", () => { From 21f77b5868b63e58d334516c929a8c8a27c98187 Mon Sep 17 00:00:00 2001 From: Liz Date: Sat, 24 Jan 2026 22:40:08 -0500 Subject: [PATCH 10/10] fixing tests for coverage --- .../__tests__/grade_breakdown_chart.test.jsx | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx b/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx index d8f9e87d6c..11a818b5a0 100644 --- a/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx +++ b/app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx @@ -21,18 +21,31 @@ jest.mock("../Assessment_Chart/core_statistics", () => ({ jest.mock("../table/table", () => { return function MockTable(props) { const firstRow = props.data[0]; - const averageColumn = props.columns.find( - // find the "average" column - col => col.id === "average" || col.accessorKey === "average" - ); + + // 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
- {averageColumn?.cell?.({ - row: {original: firstRow}, - })} + {renderedCells} + {props.renderSubComponent && props.renderSubComponent({row: {original: firstRow}})}
);