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

MarkUsProject / Markus / 25579582640

08 May 2026 09:07PM UTC coverage: 91.754% (+0.03%) from 91.726%
25579582640

Pull #7938

github

web-flow
Merge 00a015a02 into cb5a389d1
Pull Request #7938: TICKET-614: Add case sensitivy to group search

995 of 1895 branches covered (52.51%)

Branch coverage included in aggregate %.

73 of 93 new or added lines in 5 files covered. (78.49%)

129 existing lines in 4 files now uncovered.

45462 of 48737 relevant lines covered (93.28%)

131.4 hits per line

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

51.02
/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!
64
            group.inactive = true;
×
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;
×
89
    this.setState({show_hidden});
×
90
  };
91

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

102
  createAllGroups = () => {
12✔
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✔
112
    let groupings = this.groupsTable.state.selection;
×
113
    if (groupings.length === 0) {
×
114
      alert(I18n.t("groups.select_a_group"));
×
115
      return;
×
116
    } else if (!confirm(I18n.t("groups.delete_confirm"))) {
×
117
      return;
×
118
    }
119

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✔
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});
×
151
      this.fetchData();
×
152
    });
153
  };
154

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

163
  unassign = (grouping_id, student_user_name) => {
12✔
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;
×
182
    } else if (this.groupsTable.state.selection.length === 0) {
×
183
      alert(I18n.t("groups.select_a_group"));
×
184
      return;
×
185
    } else if (this.groupsTable.state.selection.length > 1) {
×
186
      alert(I18n.t("groups.select_only_one_group"));
×
187
      return;
×
188
    }
189

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

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✔
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});
×
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✔
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✔
250
    if (this.groupsTable.state.selection.length === 0) {
×
251
      alert(I18n.t("groups.select_a_group"));
×
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✔
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✔
280
    if (!confirm(I18n.t("groups.validate_confirm"))) {
×
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

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

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

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
    );
303
    fetch(url).then(this.fetchData);
×
304
  };
305

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

314
  handleCloseModal = updated => {
12✔
315
    this.setState({show_modal: false}, () => {
×
316
      if (updated) {
×
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!
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
      caseSensitive: true,
434
      columns: this.getColumns(true),
435
    };
436
  }
437

438
  componentDidUpdate(prevProps, prevState) {
439
    if (prevState.caseSensitive !== this.state.caseSensitive) {
22✔
440
      this.setState({columns: this.getColumns(this.state.caseSensitive)});
3✔
441
    }
442
  }
443

444
  toggleCaseSensitive = () => {
12✔
445
    this.setState(state => ({caseSensitive: !state.caseSensitive}));
3✔
446
  };
447

448
  groupNameFilter = caseSensitiveTextFilter({
12✔
449
    getCaseSensitive: () => this.state.caseSensitive,
57✔
450
    onToggle: this.toggleCaseSensitive,
451
  });
452

453
  getColumns = caseSensitive => {
12✔
454
    return [
15✔
455
      {
456
        accessor: "inactive",
457
        id: "inactive",
458
        width: 0,
459
        className: "rt-hidden",
460
        headerClassName: "rt-hidden",
461
        resizable: false,
462
      },
463
      {
464
        show: false,
465
        accessor: "id",
466
        id: "_id",
467
      },
468
      {
469
        Header: I18n.t("activerecord.models.group.one"),
470
        accessor: "group_name",
471
        id: "group_name",
472
        Filter: this.groupNameFilter,
473
        filterMethod: caseSensitiveStringFilterMethod(caseSensitive),
474
        Cell: row => {
475
          return (
48✔
476
            <span>
477
              <span>{row.value}</span>
478
              <a
479
                href="#"
NEW
UNCOV
480
                onClick={() => this.props.renameGroup(row.original._id, row.value)}
×
481
                title={I18n.t("groups.rename_group")}
482
              >
483
                <FontAwesomeIcon icon="fa-solid fa-pen" className="icon-right" />
484
              </a>
485
            </span>
486
          );
487
        },
488
      },
489
      {
490
        Header: I18n.t("activerecord.attributes.group.student_memberships"),
491
        accessor: "members",
492
        Cell: row => {
493
          if (row.value.length > 0 || !this.props.scanned_exam) {
48!
494
            return row.value.map(member => {
48✔
495
              let status;
496
              if (member[1] === "pending") {
48!
NEW
UNCOV
497
                status = <strong>({member[1]})</strong>;
×
498
              } else {
499
                status = member.display_label;
48✔
500
              }
501
              return (
48✔
502
                <div key={`${row.original._id}-${member[0]}`}>
503
                  {member[0]} {status}
504
                  <a
505
                    href="#"
NEW
UNCOV
506
                    onClick={() => this.props.unassign(row.original._id, member[0])}
×
507
                    title={I18n.t("delete")}
508
                  >
509
                    <FontAwesomeIcon icon="fa-solid fa-trash" className="icon-right" />
510
                  </a>
511
                </div>
512
              );
513
            });
514
          } else {
515
            // Link to assigning a student to this scanned exam
NEW
UNCOV
516
            const assign_url = Routes.assign_scans_course_assignment_groups_path(
×
517
              this.props.course_id,
518
              this.props.assignment_id,
519
              {grouping_id: row.original._id}
520
            );
NEW
UNCOV
521
            return <a href={assign_url}>{I18n.t("exam_templates.assign_scans.title")}</a>;
×
522
          }
523
        },
524
        filterMethod: (filter, row) => {
NEW
UNCOV
525
          if (!filter.value) return true;
×
NEW
UNCOV
526
          return row._original.members.some(member => member[0].includes(filter.value));
×
527
        },
528
        sortable: false,
529
      },
530
      {
531
        Header: I18n.t("groups.valid"),
532
        Cell: row => {
533
          const isValid =
534
            row.original.instructor_approved || row.original.members.length >= this.props.groupMin;
48!
535
          if (isValid) {
48!
536
            return (
48✔
537
              <a
538
                href="#"
539
                title={I18n.t("groups.is_valid")}
NEW
UNCOV
540
                onClick={() => this.props.invalidate(row.original._id)}
×
541
              >
542
                ✔
543
              </a>
544
            );
545
          }
NEW
UNCOV
546
          return (
×
547
            <a
548
              href="#"
549
              title={I18n.t("groups.is_not_valid")}
NEW
UNCOV
550
              onClick={() => this.props.validate(row.original._id)}
×
551
            >
552
              <FontAwesomeIcon icon="fa-solid fa-close" />
553
            </a>
554
          );
555
        },
556
        filterMethod: (filter, row) => {
NEW
UNCOV
557
          if (filter.value === "all") return true;
×
NEW
UNCOV
558
          const expected = filter.value === "true";
×
559
          const isValid =
NEW
UNCOV
560
            row._original.instructor_approved ||
×
561
            row._original.members.length >= this.props.groupMin;
NEW
UNCOV
562
          return isValid === expected;
×
563
        },
564
        Filter: selectFilter,
565
        filterOptions: [
566
          {value: "true", text: I18n.t("groups.is_valid")},
567
          {value: "false", text: I18n.t("groups.is_not_valid")},
568
        ],
569
        minWidth: 30,
570
        sortable: false,
571
      },
572
      {
573
        Header: this.props.extensionColumnHeader,
574
        accessor: "extension",
575
        show: !this.props.scanned_exam,
576
        Cell: row => {
577
          const timeExtension = getTimeExtension(row.original.extension, this.props.times);
48✔
578
          if (!timeExtension) {
48✔
579
            return (
27✔
580
              <a
581
                href="#"
NEW
UNCOV
582
                onClick={() => this.props.onExtensionModal(row.original.extension, false)}
×
583
                title={I18n.t("add")}
584
              >
585
                <FontAwesomeIcon icon="fa-solid fa-add" />
586
              </a>
587
            );
588
          }
589
          const lateSubmissionText = row.original.extension.apply_penalty
21!
590
            ? `(${I18n.t("groups.late_submissions_accepted")})`
591
            : "";
592
          return (
21✔
593
            <div>
NEW
UNCOV
594
              <a href="#" onClick={() => this.props.onExtensionModal(row.original.extension, true)}>
×
595
                {`${timeExtension} ${lateSubmissionText}`}
596
              </a>
597
            </div>
598
          );
599
        },
600
        sortMethod: durationSort,
601
        Filter: selectFilter,
602
        filterMethod: (filter, row) => {
603
          if (filter.value === "all") return true;
7✔
604
          const {withExtension, withLateSubmission} = JSON.parse(filter.value);
6✔
605
          // If there is an extension applied, the extension object will contain a property called hours
606
          const hasExtension = Object.hasOwn(row._original.extension, "hours");
6✔
607
          if (!withExtension) return !hasExtension;
6✔
608
          const applyPenalty = row._original.extension.apply_penalty;
4✔
609
          if (withLateSubmission) return hasExtension && applyPenalty;
4✔
610
          return hasExtension && !applyPenalty;
2✔
611
        },
612
        filterOptions: [
613
          {
614
            value: JSON.stringify({withExtension: false}),
615
            text: I18n.t("groups.groups_without_extension"),
616
          },
617
          {
618
            value: JSON.stringify({withExtension: true, withLateSubmission: true}),
619
            text: I18n.t("groups.groups_with_extension.with_late_submission"),
620
          },
621
          {
622
            value: JSON.stringify({withExtension: true, withLateSubmission: false}),
623
            text: I18n.t("groups.groups_with_extension.without_late_submission"),
624
          },
625
        ],
626
      },
627
    ];
628
  };
629

630
  static getDerivedStateFromProps(props, state) {
631
    let filtered = state.filtered.filter(group => group.id !== "inactive");
34✔
632

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

639
  onFilteredChange = filtered => {
12✔
640
    this.setState({filtered});
4✔
641
  };
642

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

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

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

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

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

776
const GroupsTable = withSelection(RawGroupsTable);
1✔
777
const StudentsTable = withSelection(RawStudentsTable);
1✔
778

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

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

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

849
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