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

MarkUsProject / Markus / 26381691124

25 May 2026 03:31AM UTC coverage: 90.285% (+0.03%) from 90.258%
26381691124

Pull #7938

github

web-flow
Merge 34449a447 into 423827af9
Pull Request #7938: TICKET-614: Add case sensitivy to group search

996 of 2177 branches covered (45.75%)

Branch coverage included in aggregate %.

55 of 75 new or added lines in 5 files covered. (73.33%)

104 existing lines in 3 files now uncovered.

45971 of 49844 relevant lines covered (92.23%)

122.82 hits per line

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

49.65
/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
      columns: this.getColumns(),
434
    };
435
  }
436

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

614
  static getDerivedStateFromProps(props, state) {
615
    let filtered = state.filtered.filter(group => group.id !== "inactive");
31✔
616

617
    if (!props.showInactive) {
31!
618
      filtered.push({id: "inactive", value: false});
31✔
619
    }
620
    return {filtered};
31✔
621
  }
622

623
  onFilteredChange = filtered => {
12✔
624
    this.setState({filtered});
7✔
625
  };
626

627
  render() {
628
    return (
31✔
629
      <CheckboxTable
630
        ref={r => (this.checkboxTable = r)}
62✔
631
        data={this.props.groups}
632
        columns={this.state.columns}
633
        defaultSorted={[
634
          {
635
            id: "group_name",
636
          },
637
        ]}
638
        loading={this.props.loading}
639
        filterable
640
        filtered={this.state.filtered}
641
        onFilteredChange={this.onFilteredChange}
642
        {...this.props.getCheckboxProps()}
643
      />
644
    );
645
  }
646
}
647

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

722
  static getDerivedStateFromProps(props, state) {
723
    let filtered = [];
24✔
724
    for (let i = 0; i < state.filtered.length; i++) {
24✔
725
      if (state.filtered[i].id !== "hidden") {
12!
726
        filtered.push(state.filtered[i]);
×
727
      }
728
    }
729
    if (!props.showHidden) {
24!
730
      filtered.push({id: "hidden", value: false});
24✔
731
    }
732
    return {filtered};
24✔
733
  }
734

735
  onFilteredChange = filtered => {
12✔
UNCOV
736
    this.setState({filtered});
×
737
  };
738

739
  render() {
740
    return (
24✔
741
      <CheckboxTable
742
        ref={r => (this.checkboxTable = r)}
48✔
743
        data={this.props.students}
744
        columns={this.state.columns}
745
        defaultSorted={[
746
          {
747
            id: "user_name",
748
          },
749
        ]}
750
        loading={this.props.loading}
751
        filterable
752
        filtered={this.state.filtered}
753
        onFilteredChange={this.onFilteredChange}
754
        {...this.props.getCheckboxProps()}
755
      />
756
    );
757
  }
758
}
759

760
const GroupsTable = withSelection(RawGroupsTable);
1✔
761
const StudentsTable = withSelection(RawStudentsTable);
1✔
762

763
class GroupsActionBox extends React.Component {
764
  render = () => {
12✔
765
    var showHiddenTooltip = null;
24✔
766
    if (this.props.hiddenStudentsCount !== null && this.props.hiddenGroupsCount !== null) {
24✔
767
      showHiddenTooltip = `${I18n.t("activerecord.attributes.grouping.inactive_students", {
12✔
768
        count: this.props.hiddenStudentsCount,
769
      })}, ${I18n.t("activerecord.attributes.grouping.inactive_groups", {
770
        count: this.props.hiddenGroupsCount,
771
      })}`;
772
    }
773

774
    return (
24✔
775
      <div className="rt-action-box">
776
        <span>
777
          <input
778
            id="show_hidden"
779
            name="show_hidden"
780
            type="checkbox"
781
            checked={this.props.showHidden}
782
            onChange={this.props.updateShowHidden}
783
            style={{marginLeft: "5px", marginRight: "5px"}}
784
          />
785
          <label title={showHiddenTooltip} htmlFor="show_hidden">
786
            {I18n.t("students.display_inactive")}
787
          </label>
788
        </span>
789
        {this.props.vcs_submit && (
24!
790
          <button onClick={this.props.handleShowAssignmentGroupUseModal}>
791
            <FontAwesomeIcon icon="fa-solid fa-recycle" />
792
            {I18n.t("groups.reuse_groups")}
793
          </button>
794
        )}
795
        <button className="" onClick={this.props.assign}>
796
          <FontAwesomeIcon icon="fa-solid fa-user-plus" />
797
          {I18n.t("groups.add_to_group")}
798
        </button>
799
        {this.props.scanned_exam && (
24!
800
          <button onClick={this.props.handleShowAutoMatchModal}>
801
            <FontAwesomeIcon icon="fa-solid fa-file-import" />
802
            {I18n.t("groups.auto_match")}
803
          </button>
804
        )}
805
        {this.props.can_create_all_groups ? (
24!
806
          <button className="" onClick={this.props.createAllGroups}>
807
            <FontAwesomeIcon icon="fa-solid fa-people-group" />
808
            {I18n.t("groups.add_all_groups")}
809
          </button>
810
        ) : undefined}
811
        <button className="" onClick={this.props.createGroup}>
812
          <FontAwesomeIcon icon="fa-solid fa-circle-plus" />
813
          {I18n.t("helpers.submit.create", {
814
            model: I18n.t("activerecord.models.group.one"),
815
          })}
816
        </button>
817
        <button className="" onClick={this.props.deleteGroups}>
818
          <FontAwesomeIcon icon="fa-solid fa-trash" />
819
          {I18n.t("groups.delete")}
820
        </button>
821
      </div>
822
    );
823
  };
824
}
825

826
export function makeGroupsManager(elem, props) {
UNCOV
827
  const root = createRoot(elem);
×
UNCOV
828
  const component = React.createRef();
×
UNCOV
829
  root.render(<GroupsManager {...props} ref={component} />);
×
UNCOV
830
  return component;
×
831
}
832

833
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