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

MarkUsProject / Markus / 26761440669

01 Jun 2026 02:31PM UTC coverage: 90.193% (-0.08%) from 90.277%
26761440669

Pull #7968

github

web-flow
Merge c92a5051d into dace57810
Pull Request #7968: Fix navigation warnings on autotest manager and starter file manager pages, closes 7641

1027 of 2226 branches covered (46.14%)

Branch coverage included in aggregate %.

11 of 11 new or added lines in 2 files covered. (100.0%)

390 existing lines in 23 files now uncovered.

46153 of 50084 relevant lines covered (92.15%)

123.24 hits per line

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

50.0
/app/javascript/Components/groups_manager.jsx
1
import React from "react";
2
import {createRoot} from "react-dom/client";
3
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
4

5
import {withSelection, CheckboxTable} from "./markus_with_selection_hoc";
6
import ExtensionModal from "./Modals/extension_modal";
7
import {
8
  caseSensitiveStringFilterMethod,
9
  caseSensitiveTextFilter,
10
  durationSort,
11
  getTimeExtension,
12
  selectFilter,
13
} from "./Helpers/table_helpers";
14
import AutoMatchModal from "./Modals/auto_match_modal";
15
import CreateGroupModal from "./Modals/create_group_modal";
16
import RenameGroupModal from "./Modals/rename_group_modal";
17
import AssignmentGroupUseModal from "./Modals/assignment_group_use_modal";
18

19
class GroupsManager extends React.Component {
20
  constructor(props) {
21
    super(props);
12✔
22
    this.state = {
12✔
23
      groups: [],
24
      students: [],
25
      show_hidden: false,
26
      hidden_students_count: 0,
27
      inactive_groups_count: 0,
28
      renameGroupingId: null,
29
      renameGroupName: "",
30
      show_modal: false,
31
      selected_extension_data: {},
32
      updating_extension: false,
33
      isAutoMatchModalOpen: false,
34
      isAssignmentGroupUseModalOpen: false,
35
      isCreateGroupModalOpen: false,
36
      isRenameGroupDialogOpen: false,
37
      examTemplates: [],
38
      loading: true,
39
      cloneAssignments: [],
40
    };
41
  }
42

43
  componentDidMount() {
44
    this.fetchData();
12✔
45
  }
46

47
  fetchData = () => {
12✔
48
    fetch(Routes.course_assignment_groups_path(this.props.course_id, this.props.assignment_id), {
12✔
49
      headers: {
50
        Accept: "application/json",
51
      },
52
    })
53
      .then(response => {
54
        if (response.ok) {
12!
55
          return response.json();
12✔
56
        }
57
      })
58
      .then(res => {
59
        this.studentsTable.resetSelection();
12✔
60
        this.groupsTable.resetSelection();
12✔
61
        var inactive_groups_count = 0;
12✔
62
        res.groups.forEach(group => {
12✔
63
          if (group.members.length && group.members.every(member => member[2])) {
24!
UNCOV
64
            group.inactive = true;
×
UNCOV
65
            inactive_groups_count += 1;
×
66
          } else {
67
            group.inactive = false;
24✔
68
          }
69
          group.members.forEach(member => {
24✔
70
            member.display_label = `(${member[1]}${
24✔
71
              member[2] ? `, ${I18n.t("activerecord.attributes.user.hidden")}` : ""
24!
72
            })`;
73
          });
74
        });
75
        this.setState({
12✔
76
          groups: res.groups,
77
          students: res.students || [],
12!
78
          loading: false,
79
          hidden_students_count: res.students.filter(student => student.hidden).length,
12✔
80
          inactive_groups_count: inactive_groups_count,
81
          examTemplates: res.exam_templates,
82
          cloneAssignments: res.clone_assignments || [],
12!
83
        });
84
      });
85
  };
86

87
  updateShowHidden = event => {
12✔
88
    let show_hidden = event.target.checked;
×
UNCOV
89
    this.setState({show_hidden});
×
90
  };
91

92
  createGroup = () => {
12✔
UNCOV
93
    if (this.props.group_name_autogenerated) {
×
UNCOV
94
      fetch(
×
95
        Routes.new_course_assignment_group_path(this.props.course_id, this.props.assignment_id)
96
      ).then(this.fetchData);
97
    } else {
UNCOV
98
      this.setState({isCreateGroupModalOpen: true});
×
99
    }
100
  };
101

102
  createAllGroups = () => {
12✔
UNCOV
103
    $.get({
×
104
      url: Routes.create_groups_when_students_work_alone_course_assignment_groups_path(
105
        this.props.course_id,
106
        this.props.assignment_id
107
      ),
108
    }).then(this.fetchData);
109
  };
110

111
  deleteGroups = () => {
12✔
UNCOV
112
    let groupings = this.groupsTable.state.selection;
×
UNCOV
113
    if (groupings.length === 0) {
×
114
      alert(I18n.t("groups.select_a_group"));
×
UNCOV
115
      return;
×
UNCOV
116
    } else if (!confirm(I18n.t("groups.delete_confirm"))) {
×
UNCOV
117
      return;
×
118
    }
119

UNCOV
120
    $.ajax(
×
121
      Routes.remove_group_course_assignment_groups_path(
122
        this.props.course_id,
123
        this.props.assignment_id
124
      ),
125
      {
126
        method: "DELETE",
127
        data: {
128
          // TODO: change param to grouping_ids
129
          grouping_id: groupings,
130
        },
131
      }
132
    ).then(this.fetchData);
133
  };
134

135
  renameGroup = (grouping_id, group_name) => {
12✔
UNCOV
136
    this.setState({
×
137
      isRenameGroupDialogOpen: true,
138
      renameGroupingId: grouping_id,
139
      renameGroupName: group_name,
140
    });
141
  };
142

143
  handleRenameGroupDialog = newGroupName => {
12✔
144
    $.post({
×
145
      url: Routes.rename_group_course_group_path(this.props.course_id, this.state.renameGroupingId),
146
      data: {
147
        new_groupname: newGroupName,
148
      },
149
    }).then(() => {
150
      this.setState({isRenameGroupDialogOpen: false});
×
UNCOV
151
      this.fetchData();
×
152
    });
153
  };
154

155
  handleCloseRenameGroupDialog = () => {
12✔
UNCOV
156
    this.setState({
×
157
      isRenameGroupDialogOpen: false,
158
      renameGroupingId: null,
159
      renameGroupName: "",
160
    });
161
  };
162

163
  unassign = (grouping_id, student_user_name) => {
12✔
UNCOV
164
    $.post({
×
165
      url: Routes.global_actions_course_assignment_groups_path(
166
        this.props.course_id,
167
        this.props.assignment_id
168
      ),
169
      data: {
170
        global_actions: "unassign",
171
        groupings: [grouping_id],
172
        students: [], // Not necessary for 'unassign'
173
        students_to_remove: [student_user_name],
174
      },
175
    }).then(this.fetchData);
176
  };
177

178
  assign = () => {
12✔
179
    if (this.studentsTable.state.selection.length === 0) {
×
180
      alert(I18n.t("groups.select_a_student"));
×
181
      return;
×
UNCOV
182
    } else if (this.groupsTable.state.selection.length === 0) {
×
UNCOV
183
      alert(I18n.t("groups.select_a_group"));
×
184
      return;
×
185
    } else if (this.groupsTable.state.selection.length > 1) {
×
UNCOV
186
      alert(I18n.t("groups.select_only_one_group"));
×
187
      return;
×
188
    }
189

UNCOV
190
    let students = this.studentsTable.state.selection;
×
UNCOV
191
    let grouping_id = this.groupsTable.state.selection[0];
×
192

UNCOV
193
    $.post({
×
194
      url: Routes.global_actions_course_assignment_groups_path(
195
        this.props.course_id,
196
        this.props.assignment_id
197
      ),
198
      data: {
199
        global_actions: "assign",
200
        groupings: [grouping_id],
201
        students: students,
202
      },
203
    }).then(this.fetchData);
204
  };
205

206
  handleCloseCreateGroupModal = () => {
12✔
207
    this.setState({
×
208
      isCreateGroupModalOpen: false,
209
    });
210
  };
211

212
  handleSubmitCreateGroup = groupName => {
12✔
UNCOV
213
    $.get({
×
214
      url: Routes.new_course_assignment_group_path(this.props.course_id, this.props.assignment_id),
215
      data: {new_group_name: groupName},
216
    }).then(() => {
217
      this.setState({isCreateGroupModalOpen: false});
×
UNCOV
218
      this.fetchData();
×
219
    });
220
  };
221

222
  handleShowAssignmentGroupUseModal = () => {
12✔
223
    this.setState({
×
224
      isAssignmentGroupUseModalOpen: true,
225
    });
226
  };
227

228
  handleCloseAssignmentGroupUseModal = () => {
12✔
229
    this.setState({
×
230
      isAssignmentGroupUseModalOpen: false,
231
    });
232
  };
233

234
  handleSubmitAssignmentGroupUseModal = selectedAssignmentId => {
12✔
UNCOV
235
    $.post({
×
236
      url: Routes.use_another_assignment_groups_course_assignment_groups_path(
237
        this.props.course_id,
238
        this.props.assignment_id
239
      ),
240
      data: {
241
        clone_assignment_id: selectedAssignmentId,
242
      },
243
    }).then(() => {
244
      this.setState({isAssignmentGroupUseModalOpen: false});
×
245
      this.fetchData();
×
246
    });
247
  };
248

249
  handleShowAutoMatchModal = () => {
12✔
UNCOV
250
    if (this.groupsTable.state.selection.length === 0) {
×
UNCOV
251
      alert(I18n.t("groups.select_a_group"));
×
UNCOV
252
      return;
×
253
    }
254

255
    this.setState({
×
256
      isAutoMatchModalOpen: true,
257
    });
258
  };
259

260
  handleCloseAutoMatchModal = () => {
12✔
261
    this.setState({
×
262
      isAutoMatchModalOpen: false,
263
    });
264
  };
265

266
  autoMatch = examTemplate => {
12✔
UNCOV
267
    $.post({
×
268
      url: Routes.auto_match_course_assignment_groups_path(
269
        this.props.course_id,
270
        this.props.assignment_id
271
      ),
272
      data: {
273
        groupings: this.groupsTable.state.selection,
274
        exam_template_id: examTemplate,
275
      },
276
    });
277
  };
278

279
  validate = grouping_id => {
12✔
UNCOV
280
    if (!confirm(I18n.t("groups.validate_confirm"))) {
×
UNCOV
281
      return;
×
282
    }
283

284
    const url = Routes.valid_grouping_course_assignment_groups_path(
×
285
      this.props.course_id,
286
      this.props.assignment_id,
287
      {grouping_id: grouping_id}
288
    );
289

UNCOV
290
    fetch(url).then(this.fetchData);
×
291
  };
292

293
  invalidate = grouping_id => {
12✔
UNCOV
294
    if (!confirm(I18n.t("groups.invalidate_confirm"))) {
×
UNCOV
295
      return;
×
296
    }
297

UNCOV
298
    const url = Routes.invalid_grouping_course_assignment_groups_path(
×
299
      this.props.course_id,
300
      this.props.assignment_id,
301
      {grouping_id: grouping_id}
302
    );
UNCOV
303
    fetch(url).then(this.fetchData);
×
304
  };
305

306
  handleShowModal = (extension_data, updating) => {
12✔
UNCOV
307
    this.setState({
×
308
      show_modal: true,
309
      selected_extension_data: extension_data,
310
      updating_extension: updating,
311
    });
312
  };
313

314
  handleCloseModal = updated => {
12✔
UNCOV
315
    this.setState({show_modal: false}, () => {
×
UNCOV
316
      if (updated) {
×
UNCOV
317
        this.fetchData();
×
318
      }
319
    });
320
  };
321

322
  extraModalInfo = () => {
12✔
323
    // Render extra modal info for timed assignments only
324
    if (this.props.timed) {
24!
UNCOV
325
      return I18n.t("assignments.timed.modal_current_duration", {
×
326
        duration: this.props.current_duration,
327
      });
328
    }
329
  };
330

331
  render() {
332
    const times = !!this.props.timed ? ["hours", "minutes"] : ["weeks", "days", "hours", "minutes"];
24!
333
    const title = !!this.props.timed
24!
334
      ? I18n.t("groups.duration_extension")
335
      : I18n.t("groups.due_date_extension");
336
    return (
24✔
337
      <div>
338
        <GroupsActionBox
339
          assign={this.assign}
340
          can_create_all_groups={this.props.can_create_all_groups}
341
          createAllGroups={this.createAllGroups}
342
          createGroup={this.createGroup}
343
          deleteGroups={this.deleteGroups}
344
          handleShowAutoMatchModal={this.handleShowAutoMatchModal}
345
          handleShowAssignmentGroupUseModal={this.handleShowAssignmentGroupUseModal}
346
          hiddenStudentsCount={this.state.loading ? null : this.state.hidden_students_count}
24✔
347
          hiddenGroupsCount={this.state.loading ? null : this.state.inactive_groups_count}
24✔
348
          scanned_exam={this.props.scanned_exam}
349
          showHidden={this.state.show_hidden}
350
          updateShowHidden={this.updateShowHidden}
351
          vcs_submit={this.props.vcs_submit}
352
        />
353
        <div className="mapping-tables">
354
          <div className="mapping-table">
355
            <StudentsTable
356
              ref={r => (this.studentsTable = r)}
48✔
357
              students={this.state.students}
358
              loading={this.state.loading}
359
              showHidden={this.state.show_hidden}
360
            />
361
          </div>
362
          <div className="mapping-table">
363
            <GroupsTable
364
              ref={r => (this.groupsTable = r)}
48✔
365
              course_id={this.props.course_id}
366
              groups={this.state.groups}
367
              loading={this.state.loading}
368
              unassign={this.unassign}
369
              renameGroup={this.renameGroup}
370
              groupMin={this.props.groupMin}
371
              validate={this.validate}
372
              invalidate={this.invalidate}
373
              scanned_exam={this.props.scanned_exam}
374
              assignment_id={this.props.assignment_id}
375
              onExtensionModal={this.handleShowModal}
376
              extensionColumnHeader={title}
377
              times={times}
378
              showInactive={this.state.show_hidden}
379
            />
380
          </div>
381
        </div>
382
        <ExtensionModal
383
          course_id={this.props.course_id}
384
          isOpen={this.state.show_modal}
385
          onRequestClose={this.handleCloseModal}
386
          weeks={this.state.selected_extension_data.weeks}
387
          days={this.state.selected_extension_data.days}
388
          hours={this.state.selected_extension_data.hours}
389
          minutes={this.state.selected_extension_data.minutes}
390
          note={this.state.selected_extension_data.note}
391
          penalty={this.state.selected_extension_data.apply_penalty}
392
          grouping_id={this.state.selected_extension_data.grouping_id}
393
          extension_id={this.state.selected_extension_data.id}
394
          updating={this.state.updating_extension}
395
          times={times}
396
          title={title}
397
          extra_info={this.extraModalInfo()}
398
          key={this.state.selected_extension_data.id} // this causes the ExtensionModal to be recreated if this value changes
399
        />
400
        <AutoMatchModal
401
          isOpen={this.state.isAutoMatchModalOpen}
402
          onRequestClose={this.handleCloseAutoMatchModal}
403
          examTemplates={this.state.examTemplates}
404
          onSubmit={this.autoMatch}
405
        />
406
        <CreateGroupModal
407
          isOpen={this.state.isCreateGroupModalOpen}
408
          onRequestClose={this.handleCloseCreateGroupModal}
409
          onSubmit={this.handleSubmitCreateGroup}
410
        />
411
        <RenameGroupModal
412
          isOpen={this.state.isRenameGroupDialogOpen}
413
          onRequestClose={this.handleCloseRenameGroupDialog}
414
          onSubmit={this.handleRenameGroupDialog}
415
          initialGroupName={this.state.renameGroupName}
416
        />
417
        <AssignmentGroupUseModal
418
          isOpen={this.state.isAssignmentGroupUseModalOpen}
419
          onRequestClose={this.handleCloseAssignmentGroupUseModal}
420
          onSubmit={this.handleSubmitAssignmentGroupUseModal}
421
          cloneAssignments={this.state.cloneAssignments}
422
        />
423
      </div>
424
    );
425
  }
426
}
427

428
class RawGroupsTable extends React.Component {
429
  constructor(props) {
430
    super(props);
12✔
431
    this.state = {
12✔
432
      filtered: [],
433
      columns: [
434
        {
435
          accessor: "inactive",
436
          id: "inactive",
437
          width: 0,
438
          className: "rt-hidden",
439
          headerClassName: "rt-hidden",
440
          resizable: false,
441
        },
442
        {
443
          show: false,
444
          accessor: "id",
445
          id: "_id",
446
        },
447
        {
448
          Header: I18n.t("activerecord.models.group.one"),
449
          accessor: "group_name",
450
          id: "group_name",
451
          Cell: row => {
452
            return (
51✔
453
              <span>
454
                <span>{row.value}</span>
455
                <a
456
                  href="#"
UNCOV
457
                  onClick={() => this.props.renameGroup(row.original._id, row.value)}
×
458
                  title={I18n.t("groups.rename_group")}
459
                >
460
                  <FontAwesomeIcon icon="fa-solid fa-pen" className="icon-right" />
461
                </a>
462
              </span>
463
            );
464
          },
465
          Filter: caseSensitiveTextFilter,
466
          filterMethod: caseSensitiveStringFilterMethod,
467
        },
468
        {
469
          Header: I18n.t("activerecord.attributes.group.student_memberships"),
470
          accessor: "members",
471
          Cell: row => {
472
            if (row.value.length > 0 || !this.props.scanned_exam) {
51!
473
              return row.value.map(member => {
51✔
474
                let status;
475
                if (member[1] === "pending") {
51!
UNCOV
476
                  status = <strong>({member[1]})</strong>;
×
477
                } else {
478
                  status = member.display_label;
51✔
479
                }
480
                return (
51✔
481
                  <div key={`${row.original._id}-${member[0]}`}>
482
                    {member[0]} {status}
483
                    <a
484
                      href="#"
UNCOV
485
                      onClick={() => this.props.unassign(row.original._id, member[0])}
×
486
                      title={I18n.t("delete")}
487
                    >
488
                      <FontAwesomeIcon icon="fa-solid fa-trash" className="icon-right" />
489
                    </a>
490
                  </div>
491
                );
492
              });
493
            } else {
494
              // Link to assigning a student to this scanned exam
UNCOV
495
              const assign_url = Routes.assign_scans_course_assignment_groups_path(
×
496
                this.props.course_id,
497
                this.props.assignment_id,
498
                {grouping_id: row.original._id}
499
              );
UNCOV
500
              return <a href={assign_url}>{I18n.t("exam_templates.assign_scans.title")}</a>;
×
501
            }
502
          },
503
          filterMethod: (filter, row) => {
UNCOV
504
            if (filter.value) {
×
UNCOV
505
              return row._original.members.some(member => member[0].includes(filter.value));
×
506
            } else {
UNCOV
507
              return true;
×
508
            }
509
          },
510
          sortable: false,
511
        },
512
        {
513
          Header: I18n.t("groups.valid"),
514
          Cell: row => {
515
            let isValid =
516
              row.original.instructor_approved ||
51!
517
              row.original.members.length >= this.props.groupMin;
518
            if (isValid) {
51!
519
              return (
51✔
520
                <a
521
                  href="#"
522
                  title={I18n.t("groups.is_valid")}
UNCOV
523
                  onClick={() => this.props.invalidate(row.original._id)}
×
524
                >
525
                  ✔
526
                </a>
527
              );
528
            } else {
UNCOV
529
              return (
×
530
                <a
531
                  href="#"
532
                  title={I18n.t("groups.is_not_valid")}
533
                  onClick={() => this.props.validate(row.original._id)}
×
534
                >
535
                  <FontAwesomeIcon icon="fa-solid fa-close" />
536
                </a>
537
              );
538
            }
539
          },
540
          filterMethod: (filter, row) => {
541
            if (filter.value === "all") {
×
UNCOV
542
              return true;
×
543
            } else {
544
              // Either 'true' or 'false'
UNCOV
545
              const val = filter.value === "true";
×
546
              let isValid =
UNCOV
547
                row._original.instructor_approved ||
×
548
                row._original.members.length >= this.props.groupMin;
UNCOV
549
              return isValid === val;
×
550
            }
551
          },
552
          Filter: selectFilter,
553
          filterOptions: [
554
            {value: "true", text: I18n.t("groups.is_valid")},
555
            {value: "false", text: I18n.t("groups.is_not_valid")},
556
          ],
557
          minWidth: 30,
558
          sortable: false,
559
        },
560
        {
561
          Header: props.extensionColumnHeader,
562
          accessor: "extension",
563
          show: !props.scanned_exam,
564
          Cell: row => {
565
            const timeExtension = getTimeExtension(row.original.extension, this.props.times);
51✔
566
            const lateSubmissionText = row.original.extension.apply_penalty
51✔
567
              ? `(${I18n.t("groups.late_submissions_accepted")})`
568
              : "";
569
            const extension = `${timeExtension} ${lateSubmissionText}`;
51✔
570

571
            if (!!timeExtension) {
51✔
572
              return (
21✔
573
                <div>
574
                  <a
575
                    href={"#"}
UNCOV
576
                    onClick={() => this.props.onExtensionModal(row.original.extension, true)}
×
577
                  >
578
                    {extension}
579
                  </a>
580
                </div>
581
              );
582
            } else {
583
              return (
30✔
584
                <a
585
                  href="#"
UNCOV
586
                  onClick={() => this.props.onExtensionModal(row.original.extension, false)}
×
587
                  title={I18n.t("add")}
588
                >
589
                  <FontAwesomeIcon icon="fa-solid fa-add" />
590
                </a>
591
              );
592
            }
593
          },
594
          sortMethod: durationSort,
595
          Filter: selectFilter,
596
          filterMethod: (filter, row) => {
597
            if (filter.value === "all") {
7✔
598
              return true;
1✔
599
            }
600
            const applyPenalty = row._original.extension.apply_penalty;
6✔
601
            const {withExtension, withLateSubmission} = JSON.parse(filter.value);
6✔
602
            // If there is an extension applied, the extension object will contain a property called hours
603
            const hasExtension = Object.hasOwn(row._original.extension, "hours");
6✔
604

605
            if (!withExtension) {
6✔
606
              return !hasExtension;
2✔
607
            }
608
            if (withLateSubmission) {
4✔
609
              return hasExtension && applyPenalty;
2✔
610
            }
611
            return hasExtension && !applyPenalty;
2✔
612
          },
613
          filterOptions: [
614
            {
615
              value: JSON.stringify({withExtension: false}),
616
              text: I18n.t("groups.groups_without_extension"),
617
            },
618
            {
619
              value: JSON.stringify({withExtension: true, withLateSubmission: true}),
620
              text: I18n.t("groups.groups_with_extension.with_late_submission"),
621
            },
622
            {
623
              value: JSON.stringify({withExtension: true, withLateSubmission: false}),
624
              text: I18n.t("groups.groups_with_extension.without_late_submission"),
625
            },
626
          ],
627
        },
628
      ],
629
    };
630
  }
631

632
  static getDerivedStateFromProps(props, state) {
633
    let filtered = state.filtered.filter(group => group.id !== "inactive");
31✔
634

635
    if (!props.showInactive) {
31!
636
      filtered.push({id: "inactive", value: false});
31✔
637
    }
638
    return {filtered};
31✔
639
  }
640

641
  onFilteredChange = filtered => {
12✔
642
    this.setState({filtered});
7✔
643
  };
644

645
  render() {
646
    return (
31✔
647
      <CheckboxTable
648
        ref={r => (this.checkboxTable = r)}
62✔
649
        data={this.props.groups}
650
        columns={this.state.columns}
651
        defaultSorted={[
652
          {
653
            id: "group_name",
654
          },
655
        ]}
656
        loading={this.props.loading}
657
        filterable
658
        filtered={this.state.filtered}
659
        onFilteredChange={this.onFilteredChange}
660
        {...this.props.getCheckboxProps()}
661
      />
662
    );
663
  }
664
}
665

666
class RawStudentsTable extends React.Component {
667
  constructor(props) {
668
    super(props);
12✔
669
    this.state = {
12✔
670
      filtered: [],
671
      columns: [
672
        {
673
          accessor: "hidden",
674
          id: "hidden",
675
          width: 0,
676
          className: "rt-hidden",
677
          headerClassName: "rt-hidden",
678
          resizable: false,
679
        },
680
        {
681
          show: false,
682
          accessor: "_id",
683
          id: "_id",
684
        },
685
        {
686
          Header: I18n.t("activerecord.attributes.user.user_name"),
687
          accessor: "user_name",
688
          id: "user_name",
689
          Cell: props =>
690
            props.original.hidden
12!
691
              ? `${props.value} (${I18n.t("activerecord.attributes.user.hidden")})`
692
              : props.value,
693
          filterMethod: (filter, row) => {
UNCOV
694
            if (filter.value) {
×
UNCOV
695
              return `${row._original.user_name}${
×
696
                row._original.hidden ? `, ${I18n.t("activerecord.attributes.user.hidden")}` : ""
×
697
              }`.includes(filter.value);
698
            } else {
UNCOV
699
              return true;
×
700
            }
701
          },
702
          sortable: true,
703
          minWidth: 90,
704
        },
705
        {
706
          Header: I18n.t("activerecord.attributes.user.last_name"),
707
          accessor: "last_name",
708
          id: "last_name",
709
        },
710
        {
711
          Header: I18n.t("activerecord.attributes.user.first_name"),
712
          accessor: "first_name",
713
          id: "first_name",
714
        },
715
        {
716
          Header: I18n.t("groups.assigned_students") + "?",
717
          accessor: "assigned",
718
          Cell: ({value}) => (value ? "✔" : ""),
12!
719
          sortable: false,
720
          minWidth: 60,
721
          filterMethod: (filter, row) => {
UNCOV
722
            if (filter.value === "all") {
×
UNCOV
723
              return true;
×
724
            } else {
725
              // Either 'true' or 'false'
UNCOV
726
              const assigned = filter.value === "true";
×
UNCOV
727
              return row._original.assigned === assigned;
×
728
            }
729
          },
730
          Filter: selectFilter,
731
          filterOptions: [
732
            {value: "true", text: I18n.t("groups.assigned_students")},
733
            {value: "false", text: I18n.t("groups.unassigned_students")},
734
          ],
735
        },
736
      ],
737
    };
738
  }
739

740
  static getDerivedStateFromProps(props, state) {
741
    let filtered = [];
24✔
742
    for (let i = 0; i < state.filtered.length; i++) {
24✔
743
      if (state.filtered[i].id !== "hidden") {
12!
UNCOV
744
        filtered.push(state.filtered[i]);
×
745
      }
746
    }
747
    if (!props.showHidden) {
24!
748
      filtered.push({id: "hidden", value: false});
24✔
749
    }
750
    return {filtered};
24✔
751
  }
752

753
  onFilteredChange = filtered => {
12✔
UNCOV
754
    this.setState({filtered});
×
755
  };
756

757
  render() {
758
    return (
24✔
759
      <CheckboxTable
760
        ref={r => (this.checkboxTable = r)}
48✔
761
        data={this.props.students}
762
        columns={this.state.columns}
763
        defaultSorted={[
764
          {
765
            id: "user_name",
766
          },
767
        ]}
768
        loading={this.props.loading}
769
        filterable
770
        filtered={this.state.filtered}
771
        onFilteredChange={this.onFilteredChange}
772
        {...this.props.getCheckboxProps()}
773
      />
774
    );
775
  }
776
}
777

778
const GroupsTable = withSelection(RawGroupsTable);
1✔
779
const StudentsTable = withSelection(RawStudentsTable);
1✔
780

781
class GroupsActionBox extends React.Component {
782
  render = () => {
12✔
783
    var showHiddenTooltip = null;
24✔
784
    if (this.props.hiddenStudentsCount !== null && this.props.hiddenGroupsCount !== null) {
24✔
785
      showHiddenTooltip = `${I18n.t("activerecord.attributes.grouping.inactive_students", {
12✔
786
        count: this.props.hiddenStudentsCount,
787
      })}, ${I18n.t("activerecord.attributes.grouping.inactive_groups", {
788
        count: this.props.hiddenGroupsCount,
789
      })}`;
790
    }
791

792
    return (
24✔
793
      <div className="rt-action-box">
794
        <span>
795
          <input
796
            id="show_hidden"
797
            name="show_hidden"
798
            type="checkbox"
799
            checked={this.props.showHidden}
800
            onChange={this.props.updateShowHidden}
801
            style={{marginLeft: "5px", marginRight: "5px"}}
802
          />
803
          <label title={showHiddenTooltip} htmlFor="show_hidden">
804
            {I18n.t("students.display_inactive")}
805
          </label>
806
        </span>
807
        {this.props.vcs_submit && (
24!
808
          <button onClick={this.props.handleShowAssignmentGroupUseModal}>
809
            <FontAwesomeIcon icon="fa-solid fa-recycle" />
810
            {I18n.t("groups.reuse_groups")}
811
          </button>
812
        )}
813
        <button className="" onClick={this.props.assign}>
814
          <FontAwesomeIcon icon="fa-solid fa-user-plus" />
815
          {I18n.t("groups.add_to_group")}
816
        </button>
817
        {this.props.scanned_exam && (
24!
818
          <button onClick={this.props.handleShowAutoMatchModal}>
819
            <FontAwesomeIcon icon="fa-solid fa-file-import" />
820
            {I18n.t("groups.auto_match")}
821
          </button>
822
        )}
823
        {this.props.can_create_all_groups ? (
24!
824
          <button className="" onClick={this.props.createAllGroups}>
825
            <FontAwesomeIcon icon="fa-solid fa-people-group" />
826
            {I18n.t("groups.add_all_groups")}
827
          </button>
828
        ) : undefined}
829
        <button className="" onClick={this.props.createGroup}>
830
          <FontAwesomeIcon icon="fa-solid fa-circle-plus" />
831
          {I18n.t("helpers.submit.create", {
832
            model: I18n.t("activerecord.models.group.one"),
833
          })}
834
        </button>
835
        <button className="" onClick={this.props.deleteGroups}>
836
          <FontAwesomeIcon icon="fa-solid fa-trash" />
837
          {I18n.t("groups.delete")}
838
        </button>
839
      </div>
840
    );
841
  };
842
}
843

844
export function makeGroupsManager(elem, props) {
UNCOV
845
  const root = createRoot(elem);
×
UNCOV
846
  const component = React.createRef();
×
UNCOV
847
  root.render(<GroupsManager {...props} ref={component} />);
×
UNCOV
848
  return component;
×
849
}
850

851
export {GroupsManager};
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