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

MarkUsProject / Markus / 24943493688

25 Apr 2026 11:44PM UTC coverage: 91.723% (-0.002%) from 91.725%
24943493688

Pull #7919

github

web-flow
Merge b67e610cd into 303bea206
Pull Request #7919: Improved performance of Table component

967 of 1866 branches covered (51.82%)

Branch coverage included in aggregate %.

37 of 38 new or added lines in 5 files covered. (97.37%)

5 existing lines in 1 file now uncovered.

45374 of 48657 relevant lines covered (93.25%)

130.47 hits per line

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

86.59
/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();
14✔
15
    const initialData = {students: [], sections: {}, counts: {all: 0, active: 0, inactive: 0}};
13✔
16
    this.state = {
13✔
17
      columns: [],
18
      data: initialData,
19
      loading: true,
20
      rowSelection: {},
21
      columns: this.getColumns(initialData),
22
    };
23
  }
24

25
  componentDidMount() {
26
    this.fetchData();
13✔
27
  }
28

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

45
  /* Called when an action is run */
46
  onSubmit = event => {
13✔
47
    event.preventDefault();
1✔
48

49
    // Convert rowSelection object to array of student IDs
50
    const selectedIds = Object.keys(this.state.rowSelection).filter(
1✔
UNCOV
51
      key => this.state.rowSelection[key]
×
52
    );
53

54
    const data = {
1✔
55
      student_ids: selectedIds,
56
      bulk_action: this.actionBox.state.action,
57
      grace_credits: this.actionBox.state.grace_credits,
58
      section: this.actionBox.state.section,
59
    };
60

61
    this.setState({loading: true});
1✔
62
    $.ajax({
1✔
63
      method: "patch",
64
      url: Routes.bulk_modify_course_students_path(this.props.course_id),
65
      data: data,
66
    }).then(this.fetchData);
67
  };
68

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

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

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

187
  initialTableState = {
13✔
188
    sorting: [
189
      {
190
        id: "user_name",
191
      },
192
    ],
193
  };
194

195
  getRowId = row => row._id;
13✔
196

197
  onRowSelectionChange = updater => {
13✔
NEW
UNCOV
198
    this.setState(prevState => ({
×
199
      rowSelection: typeof updater === "function" ? updater(prevState.rowSelection) : updater,
×
200
    }));
201
  };
202

203
  render() {
204
    const {data, loading, rowSelection} = this.state;
18✔
205
    const selectedCount = Object.keys(rowSelection).filter(key => rowSelection[key]).length;
18✔
206

207
    return (
18✔
208
      <div data-testid={"raw_student_table"}>
209
        <StudentsActionBox
210
          ref={r => (this.actionBox = r)}
36✔
211
          sections={data.sections}
212
          disabled={selectedCount === 0}
213
          onSubmit={this.onSubmit}
214
          authenticity_token={this.props.authenticity_token}
215
        />
216
        <Table
217
          loading={loading}
218
          data={data.students}
219
          columns={this.state.columns}
220
          enableRowSelection={true}
221
          rowSelection={rowSelection}
222
          onRowSelectionChange={this.onRowSelectionChange}
223
          getRowId={this.getRowId}
224
          initialState={this.iniitialTableState}
225
          noDataText={I18n.t("students.empty_table")}
226
        />
227
      </div>
228
    );
229
  }
230
}
231

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

243
  inputChanged = event => {
24✔
244
    this.setState({[event.target.name]: event.target.value});
2✔
245
  };
246

247
  actionChanged = event => {
24✔
248
    this.setState({action: event.target.value});
5✔
249
  };
250

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

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

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

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

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

328
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