• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MarkUsProject / Markus / 22548040118

01 Mar 2026 04:56PM UTC coverage: 91.662% (-0.02%) from 91.682%
22548040118

Pull #7826

github

web-flow
Merge 0375b38d4 into 64ff32a09
Pull Request #7826: Upgrade Student Table to react-table v8

939 of 1836 branches covered (51.14%)

Branch coverage included in aggregate %.

36 of 39 new or added lines in 3 files covered. (92.31%)

6 existing lines in 2 files now uncovered.

45012 of 48295 relevant lines covered (93.2%)

129.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.75
/app/javascript/Components/student_table.jsx
1
import React from "react";
2
import {createRoot} from "react-dom/client";
3
import PropTypes from "prop-types";
4

5
import Table from "./table/table";
6
import {createColumnHelper} from "@tanstack/react-table";
7
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
8
import {faPencil, faTrashCan} from "@fortawesome/free-solid-svg-icons";
9

10
const columnHelper = createColumnHelper();
2✔
11

12
class StudentTable extends React.Component {
13
  constructor() {
14
    super();
13✔
15
    this.state = {
12✔
16
      data: {
17
        students: [],
18
        sections: {},
19
        counts: {all: 0, active: 0, inactive: 0},
20
      },
21
      loading: true,
22
      rowSelection: {},
23
    };
24
  }
25

26
  componentDidMount() {
27
    this.fetchData();
12✔
28
  }
29

30
  fetchData = () => {
12✔
31
    fetch(Routes.course_students_path(this.props.course_id), {
13✔
32
      headers: {
33
        Accept: "application/json",
34
      },
35
    })
36
      .then(response => {
37
        if (response.ok) {
12!
38
          return response.json();
12✔
39
        }
40
      })
41
      .then(res => {
42
        this.setState({
12✔
43
          data: res,
44
          loading: false,
45
        });
46
      });
47
  };
48

49
  /* Called when an action is run */
50
  onSubmit = event => {
12✔
UNCOV
51
    event.preventDefault();
×
52

53
    // Convert rowSelection object to array of student IDs
NEW
54
    const selectedIds = Object.keys(this.state.rowSelection).filter(
×
NEW
55
      key => this.state.rowSelection[key]
×
56
    );
57

UNCOV
58
    const data = {
×
59
      student_ids: selectedIds,
60
      bulk_action: this.actionBox.state.action,
61
      grace_credits: this.actionBox.state.grace_credits,
62
      section: this.actionBox.state.section,
63
    };
64

UNCOV
65
    this.setState({loading: true});
×
UNCOV
66
    $.ajax({
×
67
      method: "patch",
68
      url: Routes.bulk_modify_course_students_path(this.props.course_id),
69
      data: data,
70
    }).then(this.fetchData);
71
  };
72

73
  removeStudent = student_id => {
12✔
74
    fetch(Routes.course_student_path(this.props.course_id, student_id), {
1✔
75
      method: "DELETE",
76
      headers: {
77
        "Content-Type": "application/json",
78
        "X-CSRF-Token": document.querySelector('[name="csrf-token"]').content,
79
      },
80
    })
81
      .then(response => {
82
        if (response.ok) {
1!
83
          this.fetchData();
1✔
84
        }
85
      })
86
      .catch(error => {
87
        console.error("Error removing student:", error);
×
88
      });
89
  };
90

91
  getColumns = () => {
12✔
92
    const {data} = this.state;
20✔
93

94
    return [
20✔
95
      columnHelper.accessor("user_name", {
96
        header: I18n.t("activerecord.attributes.user.user_name"),
97
        id: "user_name",
98
        minSize: 120,
99
      }),
100
      columnHelper.accessor("first_name", {
101
        header: I18n.t("activerecord.attributes.user.first_name"),
102
        minSize: 120,
103
      }),
104
      columnHelper.accessor("last_name", {
105
        header: I18n.t("activerecord.attributes.user.last_name"),
106
        minSize: 120,
107
      }),
108
      columnHelper.accessor("email", {
109
        header: I18n.t("activerecord.attributes.user.email"),
110
        minSize: 150,
111
      }),
112
      columnHelper.accessor("id_number", {
113
        header: I18n.t("activerecord.attributes.user.id_number"),
114
        minSize: 90,
115
        meta: {
116
          className: "number",
117
        },
118
      }),
119
      columnHelper.accessor(row => data.sections[row.section] || "", {
3✔
120
        header: I18n.t("activerecord.models.section", {count: 1}),
121
        id: "section",
122
        enableColumnFilter: Boolean(Object.keys(data.sections).length),
123
        filterFn: "equals",
124
        meta: {
125
          filterVariant: "select",
126
        },
127
      }),
128
      columnHelper.accessor("remaining_grace_credits", {
129
        header: I18n.t("activerecord.attributes.user.grace_credits"),
130
        id: "grace_credits",
131
        cell: ({getValue, row}) => {
132
          return `${getValue()} / ${row.original.grace_credits}`;
6✔
133
        },
134
        minSize: 90,
135
        filterFn: (row, columnId, filterValue) => {
136
          if (filterValue === "" || isNaN(filterValue)) {
4✔
137
            return true;
2✔
138
          }
139
          return row.getValue(columnId) === Number(filterValue);
2✔
140
        },
141
        meta: {
142
          className: "number",
143
          filterVariant: "number",
144
        },
145
      }),
146
      columnHelper.accessor(
147
        row => (row.hidden ? I18n.t("roles.inactive") : I18n.t("roles.active")),
3!
148
        {
149
          header: I18n.t("roles.active") + "?",
150
          id: "hidden",
151
          filterFn: "equals",
152
          meta: {
153
            filterVariant: "select",
154
          },
155
        }
156
      ),
157
      columnHelper.accessor("_id", {
158
        header: I18n.t("actions"),
159
        cell: ({getValue}) => {
160
          const id = getValue();
6✔
161

162
          return (
6✔
163
            <>
164
              <span>
165
                <a
166
                  href={Routes.edit_course_student_path(this.props.course_id, id)}
167
                  aria-label={I18n.t("edit")}
168
                  title={I18n.t("edit")}
169
                >
170
                  <FontAwesomeIcon icon={faPencil} />
171
                </a>
172
              </span>
173
              &nbsp;|&nbsp;
174
              <span>
175
                <a
176
                  href="#"
177
                  onClick={() => this.removeStudent(id)}
1✔
178
                  aria-label={I18n.t("remove")}
179
                  title={I18n.t("remove")}
180
                >
181
                  <FontAwesomeIcon icon={faTrashCan} />
182
                </a>
183
              </span>
184
            </>
185
          );
186
        },
187
        enableSorting: false,
188
        enableColumnFilter: false,
189
      }),
190
    ];
191
  };
192

193
  render() {
194
    const {data, loading, rowSelection} = this.state;
16✔
195
    const selectedCount = Object.keys(rowSelection).filter(key => rowSelection[key]).length;
16✔
196

197
    return (
16✔
198
      <div data-testid={"raw_student_table"}>
199
        <StudentsActionBox
200
          ref={r => (this.actionBox = r)}
32✔
201
          sections={data.sections}
202
          disabled={selectedCount === 0}
203
          onSubmit={this.onSubmit}
204
          authenticity_token={this.props.authenticity_token}
205
        />
206
        <Table
207
          loading={loading}
208
          data={data.students}
209
          columns={this.getColumns()}
210
          enableRowSelection={true}
211
          rowSelection={rowSelection}
212
          onRowSelectionChange={updater => {
NEW
213
            this.setState(prevState => ({
×
214
              rowSelection:
215
                typeof updater === "function" ? updater(prevState.rowSelection) : updater,
×
216
            }));
217
          }}
218
          getRowId={row => row._id}
3✔
219
          initialState={{
220
            sorting: [
221
              {
222
                id: "user_name",
223
              },
224
            ],
225
          }}
226
          noDataText={I18n.t("students.empty_table")}
227
        />
228
      </div>
229
    );
230
  }
231
}
232

233
class StudentsActionBox extends React.Component {
234
  constructor() {
235
    super();
19✔
236
    this.state = {
19✔
237
      action: "give_grace_credits",
238
      grace_credits: 0,
239
      selected_section: "",
240
      button_disabled: false,
241
    };
242
  }
243

244
  inputChanged = event => {
19✔
245
    this.setState({[event.target.name]: event.target.value});
×
246
  };
247

248
  actionChanged = event => {
19✔
249
    this.setState({action: event.target.value});
3✔
250
  };
251

252
  render = () => {
19✔
253
    let optionalInputBox = null;
26✔
254
    if (this.state.action === "give_grace_credits") {
26✔
255
      optionalInputBox = (
24✔
256
        <input
257
          type="number"
258
          name="grace_credits"
259
          value={this.state.grace_credits}
260
          onChange={this.inputChanged}
261
        />
262
      );
263
    } else if (this.state.action === "update_section") {
2!
264
      if (Object.keys(this.props.sections).length > 0) {
2✔
265
        const section_options = Object.entries(this.props.sections).map(section => (
1✔
266
          <option key={section[0]} value={section[0]}>
2✔
267
            {section[1]}
268
          </option>
269
        ));
270
        optionalInputBox = (
1✔
271
          <select
272
            name="section"
273
            value={this.state.section}
274
            onChange={this.inputChanged}
275
            data-testid={"student_action_box_update_section"}
276
          >
277
            <option key={"none"} value={""}>
278
              {I18n.t("students.instructor_actions.no_section")}
279
            </option>
280
            {section_options}
281
          </select>
282
        );
283
      } else {
284
        optionalInputBox = <span>{I18n.t("sections.none")}</span>;
1✔
285
      }
286
    }
287

288
    return (
26✔
289
      <form onSubmit={this.props.onSubmit} data-testid={"student_action_box"}>
290
        <select
291
          value={this.state.action}
292
          onChange={this.actionChanged}
293
          data-testid={"student_action_box_select"}
294
        >
295
          <option value="give_grace_credits">
296
            {I18n.t("students.instructor_actions.give_grace_credits")}
297
          </option>
298
          <option value="update_section">
299
            {I18n.t("students.instructor_actions.update_section")}
300
          </option>
301
          <option value="hide">{I18n.t("students.instructor_actions.mark_inactive")}</option>
302
          <option value="unhide">{I18n.t("students.instructor_actions.mark_active")}</option>
303
        </select>
304
        {optionalInputBox}
305
        <input type="submit" disabled={this.props.disabled} value={I18n.t("apply")} />
306
        <input type="hidden" name="authenticity_token" value={this.props.authenticity_token} />
307
      </form>
308
    );
309
  };
310
}
311

312
StudentsActionBox.propTypes = {
2✔
313
  onSubmit: PropTypes.func,
314
  disabled: PropTypes.bool,
315
  authenticity_token: PropTypes.string,
316
  sections: PropTypes.object,
317
};
318

319
StudentTable.propTypes = {
2✔
320
  course_id: PropTypes.number,
321
  authenticity_token: PropTypes.string,
322
};
323

324
function makeStudentTable(elem, props) {
UNCOV
325
  const root = createRoot(elem);
×
326
  root.render(<StudentTable {...props} />);
×
327
}
328

329
export {StudentTable, StudentsActionBox, makeStudentTable};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc