From 7ac99201045a53aa33b813d2a963f854de083c72 Mon Sep 17 00:00:00 2001 From: ch-iv Date: Wed, 4 Jun 2025 23:53:49 -0400 Subject: [PATCH 1/3] Design improvement for student, ta, and instructor tables when the table is empty --- Changelog.md | 1 + app/assets/stylesheets/common/core.scss | 7 +++++++ .../Components/__tests__/instructor_table.test.jsx | 2 +- app/javascript/Components/__tests__/student_table.test.jsx | 2 +- app/javascript/Components/__tests__/ta_table.test.jsx | 2 +- app/javascript/Components/instructor_table.jsx | 2 ++ app/javascript/Components/student_table.jsx | 5 +++++ app/javascript/Components/ta_table.jsx | 2 ++ app/javascript/Components/table_no_data.jsx | 5 +++++ config/locales/views/users/en.yml | 3 +++ 10 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 app/javascript/Components/table_no_data.jsx diff --git a/Changelog.md b/Changelog.md index 423fe0417d..59c3b5b9b0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,7 @@ ### 🚨 Breaking changes ### ✨ New features and improvements +- Design improvement for student, ta, and instructor tables when the table is empty. (#7557) ### 🐛 Bug fixes diff --git a/app/assets/stylesheets/common/core.scss b/app/assets/stylesheets/common/core.scss index b495977600..e480d6a451 100644 --- a/app/assets/stylesheets/common/core.scss +++ b/app/assets/stylesheets/common/core.scss @@ -457,6 +457,13 @@ th.required::after { width: 100%; } +.rt-no-data { + background-color: $background-support; + margin: 0; + padding: 1em 0; + text-align: center; +} + // Styling patches for react-keyed-file-browser .div.rendered-react-keyed-file-browser div.action-bar ul.item-actions { overflow: auto; diff --git a/app/javascript/Components/__tests__/instructor_table.test.jsx b/app/javascript/Components/__tests__/instructor_table.test.jsx index c49929de07..91b7b13012 100644 --- a/app/javascript/Components/__tests__/instructor_table.test.jsx +++ b/app/javascript/Components/__tests__/instructor_table.test.jsx @@ -87,7 +87,7 @@ describe("For the InstructorTable's display of instructors", () => { }); it("No rows found is shown", async () => { - await screen.findByText("No rows found"); + await screen.findByText(I18n.t("instructors.empty_table")); }); }); }); diff --git a/app/javascript/Components/__tests__/student_table.test.jsx b/app/javascript/Components/__tests__/student_table.test.jsx index caffca2cab..2d48bbfe0c 100644 --- a/app/javascript/Components/__tests__/student_table.test.jsx +++ b/app/javascript/Components/__tests__/student_table.test.jsx @@ -264,7 +264,7 @@ describe("For the StudentTable's display of students", () => { }); it("No rows found is shown", async () => { - await screen.findByText("No rows found"); + await screen.findByText(I18n.t("students.empty_table")); }); }); diff --git a/app/javascript/Components/__tests__/ta_table.test.jsx b/app/javascript/Components/__tests__/ta_table.test.jsx index f99342bb1f..43e02a2771 100644 --- a/app/javascript/Components/__tests__/ta_table.test.jsx +++ b/app/javascript/Components/__tests__/ta_table.test.jsx @@ -99,7 +99,7 @@ describe("For the TATable's display of TAs", () => { }); it("No rows found is shown", async () => { - await screen.findByText("No rows found"); + await screen.findByText(I18n.t("tas.empty_table")); }); }); diff --git a/app/javascript/Components/instructor_table.jsx b/app/javascript/Components/instructor_table.jsx index 9b6e6a5aac..3604a27cdf 100644 --- a/app/javascript/Components/instructor_table.jsx +++ b/app/javascript/Components/instructor_table.jsx @@ -4,6 +4,7 @@ import PropTypes from "prop-types"; import ReactTable from "react-table"; import {selectFilter} from "./Helpers/table_helpers"; +import {tableNoDataComponent} from "./table_no_data"; class InstructorTable extends React.Component { constructor() { @@ -100,6 +101,7 @@ class InstructorTable extends React.Component { ]} filterable loading={this.state.loading} + NoDataComponent={() => tableNoDataComponent(I18n.t("instructors.empty_table"))} /> ); } diff --git a/app/javascript/Components/student_table.jsx b/app/javascript/Components/student_table.jsx index 0400efd7fc..cf07687ce2 100644 --- a/app/javascript/Components/student_table.jsx +++ b/app/javascript/Components/student_table.jsx @@ -4,6 +4,7 @@ import PropTypes from "prop-types"; import {CheckboxTable, withSelection} from "./markus_with_selection_hoc"; import {selectFilter} from "./Helpers/table_helpers"; +import {tableNoDataComponent} from "./table_no_data"; class RawStudentTable extends React.Component { constructor() { @@ -221,6 +222,7 @@ class RawStudentTable extends React.Component { ]} filterable loading={loading} + NoDataComponent={() => tableNoDataComponent(I18n.t("students.empty_table"))} {...this.props.getCheckboxProps()} /> @@ -306,6 +308,7 @@ class StudentsActionBox extends React.Component { ); }; } + StudentsActionBox.propTypes = { onSubmit: PropTypes.func, disabled: PropTypes.bool, @@ -321,8 +324,10 @@ RawStudentTable.propTypes = { }; let StudentTable = withSelection(RawStudentTable); + function makeStudentTable(elem, props) { const root = createRoot(elem); root.render(); } + export {StudentTable, StudentsActionBox, makeStudentTable}; diff --git a/app/javascript/Components/ta_table.jsx b/app/javascript/Components/ta_table.jsx index 255e784cab..95c07f44ed 100644 --- a/app/javascript/Components/ta_table.jsx +++ b/app/javascript/Components/ta_table.jsx @@ -4,6 +4,7 @@ import PropTypes from "prop-types"; import ReactTable from "react-table"; import {selectFilter} from "./Helpers/table_helpers"; +import {tableNoDataComponent} from "./table_no_data"; class TATable extends React.Component { constructor() { @@ -127,6 +128,7 @@ class TATable extends React.Component { ]} filterable loading={this.state.loading} + NoDataComponent={() => tableNoDataComponent(I18n.t("tas.empty_table"))} /> ); } diff --git a/app/javascript/Components/table_no_data.jsx b/app/javascript/Components/table_no_data.jsx new file mode 100644 index 0000000000..1a034b95da --- /dev/null +++ b/app/javascript/Components/table_no_data.jsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const tableNoDataComponent = empty_table_message => { + return

{empty_table_message}

; +}; diff --git a/config/locales/views/users/en.yml b/config/locales/views/users/en.yml index f10d09081f..e96d684382 100644 --- a/config/locales/views/users/en.yml +++ b/config/locales/views/users/en.yml @@ -5,6 +5,7 @@ en: help: manage_instructors: Administrators are usually instructors. They have full control over the MarkUs instance, including making assignments, assigning graders, and releasing marks. new: Add an Instructor + empty_table: No instructors found roles: active: Active inactive: Inactive @@ -30,12 +31,14 @@ en: not_successfully_added_message_3: Student names are alphanumeric. not_successfully_added_message_4: Each field is separated by a comma (no spaces). not_successfully_added_message_5: Each line has the required fields specified near the upload form. + empty_table: No students found tas: display_inactive: Display inactive graders edit: Edit a Grader help: manage_graders: Create graders. new: Add a Grader + empty_table: No graders found users: account_settings: Account Settings api_key: From db2324403fbc2c950eaa19f2797800e64342f6db Mon Sep 17 00:00:00 2001 From: ch-iv Date: Thu, 5 Jun 2025 08:10:37 -0400 Subject: [PATCH 2/3] Normalize translation keys --- config/locales/views/users/en.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/locales/views/users/en.yml b/config/locales/views/users/en.yml index e96d684382..16190ca1af 100644 --- a/config/locales/views/users/en.yml +++ b/config/locales/views/users/en.yml @@ -2,10 +2,10 @@ en: instructors: edit: Edit an Instructor + empty_table: No instructors found help: manage_instructors: Administrators are usually instructors. They have full control over the MarkUs instance, including making assignments, assigning graders, and releasing marks. new: Add an Instructor - empty_table: No instructors found roles: active: Active inactive: Inactive @@ -13,6 +13,7 @@ en: students: display_inactive: Display inactive students edit: Edit a Student + empty_table: No students found errors: group_creation_failure: Sorry! For some reason, your group could not be created. Please wait a few seconds, then hit refresh to try again. If you come back to this page, you should inform the course instructor. grouping_creation_failure: Sorry! For some reason, your grouping could not be created. Please wait a few seconds, and hit refresh to try again. If you come back to this page, you should inform the course instructor. @@ -31,14 +32,13 @@ en: not_successfully_added_message_3: Student names are alphanumeric. not_successfully_added_message_4: Each field is separated by a comma (no spaces). not_successfully_added_message_5: Each line has the required fields specified near the upload form. - empty_table: No students found tas: display_inactive: Display inactive graders edit: Edit a Grader + empty_table: No graders found help: manage_graders: Create graders. new: Add a Grader - empty_table: No graders found users: account_settings: Account Settings api_key: From bf628c0338632cd1a39d8e5c452bb36f1edc9a61 Mon Sep 17 00:00:00 2001 From: ch-iv Date: Sun, 8 Jun 2025 22:20:40 -0400 Subject: [PATCH 3/3] Implement custom no data component for all tables --- .../Components/Helpers/table_helpers.jsx | 4 ++++ .../__tests__/default_react_table.test.jsx | 16 ++++++++++++++++ app/javascript/Components/instructor_table.jsx | 3 +-- app/javascript/Components/student_table.jsx | 3 +-- app/javascript/Components/ta_table.jsx | 3 +-- app/javascript/Components/table_no_data.jsx | 5 ----- app/javascript/common/react_config.jsx | 8 +++++++- 7 files changed, 30 insertions(+), 12 deletions(-) create mode 100644 app/javascript/Components/__tests__/default_react_table.test.jsx delete mode 100644 app/javascript/Components/table_no_data.jsx diff --git a/app/javascript/Components/Helpers/table_helpers.jsx b/app/javascript/Components/Helpers/table_helpers.jsx index d77ca63f4c..978facb04c 100644 --- a/app/javascript/Components/Helpers/table_helpers.jsx +++ b/app/javascript/Components/Helpers/table_helpers.jsx @@ -229,3 +229,7 @@ export function getMarkingStates(data) { }); return markingStates; } + +export function customNoDataComponent({children}) { + return

{children}

; +} diff --git a/app/javascript/Components/__tests__/default_react_table.test.jsx b/app/javascript/Components/__tests__/default_react_table.test.jsx new file mode 100644 index 0000000000..c711fabc1d --- /dev/null +++ b/app/javascript/Components/__tests__/default_react_table.test.jsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import {render, screen, fireEvent, within} from "@testing-library/react"; +import ReactTable from "react-table"; + +describe("Default React Table", () => { + it("shows the default no data text when no data is provided", async () => { + render(); + await screen.findByText("No rows found"); + }); + + it("shows a custom no data text when set", async () => { + const customNoDataText = "custom no data text"; + render(); + await screen.findByText(customNoDataText); + }); +}); diff --git a/app/javascript/Components/instructor_table.jsx b/app/javascript/Components/instructor_table.jsx index 3604a27cdf..2610388c0e 100644 --- a/app/javascript/Components/instructor_table.jsx +++ b/app/javascript/Components/instructor_table.jsx @@ -4,7 +4,6 @@ import PropTypes from "prop-types"; import ReactTable from "react-table"; import {selectFilter} from "./Helpers/table_helpers"; -import {tableNoDataComponent} from "./table_no_data"; class InstructorTable extends React.Component { constructor() { @@ -101,7 +100,7 @@ class InstructorTable extends React.Component { ]} filterable loading={this.state.loading} - NoDataComponent={() => tableNoDataComponent(I18n.t("instructors.empty_table"))} + noDataText={I18n.t("instructors.empty_table")} /> ); } diff --git a/app/javascript/Components/student_table.jsx b/app/javascript/Components/student_table.jsx index cf07687ce2..46e4eb1e94 100644 --- a/app/javascript/Components/student_table.jsx +++ b/app/javascript/Components/student_table.jsx @@ -4,7 +4,6 @@ import PropTypes from "prop-types"; import {CheckboxTable, withSelection} from "./markus_with_selection_hoc"; import {selectFilter} from "./Helpers/table_helpers"; -import {tableNoDataComponent} from "./table_no_data"; class RawStudentTable extends React.Component { constructor() { @@ -222,7 +221,7 @@ class RawStudentTable extends React.Component { ]} filterable loading={loading} - NoDataComponent={() => tableNoDataComponent(I18n.t("students.empty_table"))} + noDataText={I18n.t("students.empty_table")} {...this.props.getCheckboxProps()} /> diff --git a/app/javascript/Components/ta_table.jsx b/app/javascript/Components/ta_table.jsx index 95c07f44ed..af9de4ffca 100644 --- a/app/javascript/Components/ta_table.jsx +++ b/app/javascript/Components/ta_table.jsx @@ -4,7 +4,6 @@ import PropTypes from "prop-types"; import ReactTable from "react-table"; import {selectFilter} from "./Helpers/table_helpers"; -import {tableNoDataComponent} from "./table_no_data"; class TATable extends React.Component { constructor() { @@ -128,7 +127,7 @@ class TATable extends React.Component { ]} filterable loading={this.state.loading} - NoDataComponent={() => tableNoDataComponent(I18n.t("tas.empty_table"))} + noDataText={I18n.t("tas.empty_table")} /> ); } diff --git a/app/javascript/Components/table_no_data.jsx b/app/javascript/Components/table_no_data.jsx deleted file mode 100644 index 1a034b95da..0000000000 --- a/app/javascript/Components/table_no_data.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from "react"; - -export const tableNoDataComponent = empty_table_message => { - return

{empty_table_message}

; -}; diff --git a/app/javascript/common/react_config.jsx b/app/javascript/common/react_config.jsx index 0916542f37..5eb84d9973 100644 --- a/app/javascript/common/react_config.jsx +++ b/app/javascript/common/react_config.jsx @@ -1,7 +1,12 @@ import {ReactTableDefaults} from "react-table"; import {I18n} from "i18n-js"; import translations from "translations.json"; -import {defaultSort, stringFilterMethod, textFilter} from "../Components/Helpers/table_helpers"; +import { + defaultSort, + stringFilterMethod, + textFilter, + customNoDataComponent, +} from "../Components/Helpers/table_helpers"; const i18n = new I18n(translations); @@ -15,6 +20,7 @@ Object.assign(ReactTableDefaults, { defaultSortMethod: defaultSort, defaultFilterMethod: stringFilterMethod, FilterComponent: textFilter, + NoDataComponent: customNoDataComponent, }); Object.assign(ReactTableDefaults.column, {