Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
Original file line number Diff line number Diff line change
@@ -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 => (
<FractionStat
numerator={info.row.original.average}
denominator={info.row.original.max_mark}
/>
),
}),
];
let summary_table = "";
if (this.props.show_stats) {
summary_table = (
<div className="flex-row-expand">
<div className="grade-breakdown-summary-table">
<ReactTable
<Table
data={this.props.summary}
columns={[
{
Header: this.props.item_name,
accessor: "name",
minWidth: 150,
},
{
Header: I18n.t("average"),
accessor: "average",
sortable: false,
filterable: false,
Cell: row => (
<FractionStat
numerator={row.original.average}
denominator={row.original.max_mark}
/>
),
},
]}
defaultSorted={[{id: "position"}]}
SubComponent={row => (
columns={columns}
initialState={{
sorting: [{id: "position"}],
columnVisibility: {position: false},
}}
getRowCanExpand={() => true}
renderSubComponent={({row}) => (
<div className="grade-stats-breakdown grid-2-col">
<CoreStatistics
average={row.original.average}
Expand Down
97 changes: 97 additions & 0 deletions app/javascript/Components/__tests__/grade_breakdown_chart.test.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div data-testid="mock-table">
<div data-testid="table-columns">{props.columns.length} columns</div>
<div data-testid="table-data">{props.data.length} rows</div>
</div>
);
};
});

jest.mock("react-chartjs-2", () => ({
Bar: () => <div data-testid="bar-chart">Bar Chart</div>,
}));

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(<GradeBreakdownChart {...defaultProps} />);
expect(screen.getByTestId("bar-chart")).toBeInTheDocument();
});

it("renders the summary table when show_stats is true", () => {
render(<GradeBreakdownChart {...defaultProps} />);
expect(screen.getByTestId("mock-table")).toBeInTheDocument();
});

it("does not render the summary table when show_stats is false", () => {
render(<GradeBreakdownChart {...defaultProps} show_stats={false} />);
expect(screen.queryByTestId("mock-table")).not.toBeInTheDocument();
});

it("passes correct number of columns to table", () => {
render(<GradeBreakdownChart {...defaultProps} />);
// 3 columns: position (hidden), name, average
expect(screen.getByTestId("table-columns")).toHaveTextContent("3 columns");
});

it("passes summary data to table", () => {
render(<GradeBreakdownChart {...defaultProps} />);
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(<GradeBreakdownChart {...emptyProps} />);
expect(screen.queryByTestId("mock-table")).not.toBeInTheDocument();
});
});
51 changes: 28 additions & 23 deletions app/javascript/Components/table/table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -138,28 +141,30 @@ export default function Table({
</div>
))}
</div>
<div className="rt-thead -filters" style={{minWidth: table.getCenterTotalSize()}}>
{table.getHeaderGroups().map(headerGroup => (
<div className="rt-tr" role="row" key={headerGroup.id}>
{headerGroup.headers.map(header => {
return (
<div
className={`rt-th ${header.column.columnDef.meta?.headerClassName || ""}`}
key={header.id}
role="columnheader"
tabIndex="-1"
style={{
width: header.getSize(),
maxWidth: header.column.columnDef.maxSize || "none",
}}
>
{header.column.getCanFilter() ? <Filter column={header.column} /> : null}
</div>
);
})}
</div>
))}
</div>
{table.getAllColumns().some(column => column.getCanFilter()) && (
<div className="rt-thead -filters" style={{minWidth: table.getCenterTotalSize()}}>
{table.getHeaderGroups().map(headerGroup => (
<div className="rt-tr" role="row" key={headerGroup.id}>
{headerGroup.headers.map(header => {
return (
<div
className={`rt-th ${header.column.columnDef.meta?.headerClassName || ""}`}
key={header.id}
role="columnheader"
tabIndex="-1"
style={{
width: header.getSize(),
maxWidth: header.column.columnDef.maxSize || "none",
}}
>
{header.column.getCanFilter() ? <Filter column={header.column} /> : null}
</div>
);
})}
</div>
))}
</div>
)}
<div className="rt-tbody" style={{minWidth: table.getCenterTotalSize()}}>
{table.getRowModel().rows.map(row => {
return (
Expand Down