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

MarkUsProject / Markus / 24593804825

18 Apr 2026 01:29AM UTC coverage: 91.723%. Remained the same
24593804825

Pull #7910

github

web-flow
Merge f948f7160 into 51d085c06
Pull Request #7910: Refactored tables to cache column definitions

955 of 1849 branches covered (51.65%)

Branch coverage included in aggregate %.

43 of 82 new or added lines in 4 files covered. (52.44%)

99 existing lines in 4 files now uncovered.

45267 of 48544 relevant lines covered (93.25%)

130.53 hits per line

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

49.66
/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 {durationSort, selectFilter, getTimeExtension} from "./Helpers/table_helpers";
8
import AutoMatchModal from "./Modals/auto_match_modal";
9
import CreateGroupModal from "./Modals/create_group_modal";
10
import RenameGroupModal from "./Modals/rename_group_modal";
11
import AssignmentGroupUseModal from "./Modals/assignment_group_use_modal";
12

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

37
  componentDidMount() {
38
    this.fetchData();
8✔
39
  }
40

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

81
  updateShowHidden = event => {
8✔
82
    let show_hidden = event.target.checked;
×
83
    this.setState({show_hidden});
×
84
  };
85

86
  createGroup = () => {
8✔
87
    if (this.props.group_name_autogenerated) {
×
88
      fetch(
×
89
        Routes.new_course_assignment_group_path(this.props.course_id, this.props.assignment_id)
90
      ).then(this.fetchData);
91
    } else {
92
      this.setState({isCreateGroupModalOpen: true});
×
93
    }
94
  };
95

96
  createAllGroups = () => {
8✔
97
    $.get({
×
98
      url: Routes.create_groups_when_students_work_alone_course_assignment_groups_path(
99
        this.props.course_id,
100
        this.props.assignment_id
101
      ),
102
    }).then(this.fetchData);
103
  };
104

105
  deleteGroups = () => {
8✔
106
    let groupings = this.groupsTable.state.selection;
×
107
    if (groupings.length === 0) {
×
108
      alert(I18n.t("groups.select_a_group"));
×
109
      return;
×
110
    } else if (!confirm(I18n.t("groups.delete_confirm"))) {
×
111
      return;
×
112
    }
113

114
    $.ajax(
×
115
      Routes.remove_group_course_assignment_groups_path(
116
        this.props.course_id,
117
        this.props.assignment_id
118
      ),
119
      {
120
        method: "DELETE",
121
        data: {
122
          // TODO: change param to grouping_ids
123
          grouping_id: groupings,
124
        },
125
      }
126
    ).then(this.fetchData);
127
  };
128

129
  renameGroup = (grouping_id, group_name) => {
8✔
130
    this.setState({
×
131
      isRenameGroupDialogOpen: true,
132
      renameGroupingId: grouping_id,
133
      renameGroupName: group_name,
134
    });
135
  };
136

137
  handleRenameGroupDialog = newGroupName => {
8✔
138
    $.post({
×
139
      url: Routes.rename_group_course_group_path(this.props.course_id, this.state.renameGroupingId),
140
      data: {
141
        new_groupname: newGroupName,
142
      },
143
    }).then(() => {
144
      this.setState({isRenameGroupDialogOpen: false});
×
145
      this.fetchData();
×
146
    });
147
  };
148

149
  handleCloseRenameGroupDialog = () => {
8✔
150
    this.setState({
×
151
      isRenameGroupDialogOpen: false,
152
      renameGroupingId: null,
153
      renameGroupName: "",
154
    });
155
  };
156

157
  unassign = (grouping_id, student_user_name) => {
8✔
158
    $.post({
×
159
      url: Routes.global_actions_course_assignment_groups_path(
160
        this.props.course_id,
161
        this.props.assignment_id
162
      ),
163
      data: {
164
        global_actions: "unassign",
165
        groupings: [grouping_id],
166
        students: [], // Not necessary for 'unassign'
167
        students_to_remove: [student_user_name],
168
      },
169
    }).then(this.fetchData);
170
  };
171

172
  assign = () => {
8✔
173
    if (this.studentsTable.state.selection.length === 0) {
×
174
      alert(I18n.t("groups.select_a_student"));
×
175
      return;
×
176
    } else if (this.groupsTable.state.selection.length === 0) {
×
177
      alert(I18n.t("groups.select_a_group"));
×
178
      return;
×
179
    } else if (this.groupsTable.state.selection.length > 1) {
×
180
      alert(I18n.t("groups.select_only_one_group"));
×
181
      return;
×
182
    }
183

184
    let students = this.studentsTable.state.selection;
×
185
    let grouping_id = this.groupsTable.state.selection[0];
×
186

187
    $.post({
×
188
      url: Routes.global_actions_course_assignment_groups_path(
189
        this.props.course_id,
190
        this.props.assignment_id
191
      ),
192
      data: {
193
        global_actions: "assign",
194
        groupings: [grouping_id],
195
        students: students,
196
      },
197
    }).then(this.fetchData);
198
  };
199

200
  handleCloseCreateGroupModal = () => {
8✔
201
    this.setState({
×
202
      isCreateGroupModalOpen: false,
203
    });
204
  };
205

206
  handleSubmitCreateGroup = groupName => {
8✔
207
    $.get({
×
208
      url: Routes.new_course_assignment_group_path(this.props.course_id, this.props.assignment_id),
209
      data: {new_group_name: groupName},
210
    }).then(() => {
211
      this.setState({isCreateGroupModalOpen: false});
×
212
      this.fetchData();
×
213
    });
214
  };
215

216
  handleShowAssignmentGroupUseModal = () => {
8✔
217
    this.setState({
×
218
      isAssignmentGroupUseModalOpen: true,
219
    });
220
  };
221

222
  handleCloseAssignmentGroupUseModal = () => {
8✔
223
    this.setState({
×
224
      isAssignmentGroupUseModalOpen: false,
225
    });
226
  };
227

228
  handleSubmitAssignmentGroupUseModal = selectedAssignmentId => {
8✔
229
    $.post({
×
230
      url: Routes.use_another_assignment_groups_course_assignment_groups_path(
231
        this.props.course_id,
232
        this.props.assignment_id
233
      ),
234
      data: {
235
        clone_assignment_id: selectedAssignmentId,
236
      },
237
    }).then(() => {
238
      this.setState({isAssignmentGroupUseModalOpen: false});
×
239
      this.fetchData();
×
240
    });
241
  };
242

243
  handleShowAutoMatchModal = () => {
8✔
244
    if (this.groupsTable.state.selection.length === 0) {
×
245
      alert(I18n.t("groups.select_a_group"));
×
246
      return;
×
247
    }
248

249
    this.setState({
×
250
      isAutoMatchModalOpen: true,
251
    });
252
  };
253

254
  handleCloseAutoMatchModal = () => {
8✔
255
    this.setState({
×
256
      isAutoMatchModalOpen: false,
257
    });
258
  };
259

260
  autoMatch = examTemplate => {
8✔
261
    $.post({
×
262
      url: Routes.auto_match_course_assignment_groups_path(
263
        this.props.course_id,
264
        this.props.assignment_id
265
      ),
266
      data: {
267
        groupings: this.groupsTable.state.selection,
268
        exam_template_id: examTemplate,
269
      },
270
    });
271
  };
272

273
  validate = grouping_id => {
8✔
274
    if (!confirm(I18n.t("groups.validate_confirm"))) {
×
275
      return;
×
276
    }
277

278
    const url = Routes.valid_grouping_course_assignment_groups_path(
×
279
      this.props.course_id,
280
      this.props.assignment_id,
281
      {grouping_id: grouping_id}
282
    );
283

284
    fetch(url).then(this.fetchData);
×
285
  };
286

287
  invalidate = grouping_id => {
8✔
288
    if (!confirm(I18n.t("groups.invalidate_confirm"))) {
×
289
      return;
×
290
    }
291

292
    const url = Routes.invalid_grouping_course_assignment_groups_path(
×
293
      this.props.course_id,
294
      this.props.assignment_id,
295
      {grouping_id: grouping_id}
296
    );
297
    fetch(url).then(this.fetchData);
×
298
  };
299

300
  handleShowModal = (extension_data, updating) => {
8✔
301
    this.setState({
×
302
      show_modal: true,
303
      selected_extension_data: extension_data,
304
      updating_extension: updating,
305
    });
306
  };
307

308
  handleCloseModal = updated => {
8✔
309
    this.setState({show_modal: false}, () => {
×
310
      if (updated) {
×
311
        this.fetchData();
×
312
      }
313
    });
314
  };
315

316
  extraModalInfo = () => {
8✔
317
    // Render extra modal info for timed assignments only
318
    if (this.props.timed) {
16!
319
      return I18n.t("assignments.timed.modal_current_duration", {
×
320
        duration: this.props.current_duration,
321
      });
322
    }
323
  };
324

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

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

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

597
            if (!withExtension) {
6✔
598
              return !hasExtension;
2✔
599
            }
600
            if (withLateSubmission) {
4✔
601
              return hasExtension && applyPenalty;
2✔
602
            }
603
            return hasExtension && !applyPenalty;
2✔
604
          },
605
          filterOptions: [
606
            {
607
              value: JSON.stringify({withExtension: false}),
608
              text: I18n.t("groups.groups_without_extension"),
609
            },
610
            {
611
              value: JSON.stringify({withExtension: true, withLateSubmission: true}),
612
              text: I18n.t("groups.groups_with_extension.with_late_submission"),
613
            },
614
            {
615
              value: JSON.stringify({withExtension: true, withLateSubmission: false}),
616
              text: I18n.t("groups.groups_with_extension.without_late_submission"),
617
            },
618
          ],
619
        },
620
      ],
621
    };
622
  }
623

624
  static getDerivedStateFromProps(props, state) {
625
    let filtered = state.filtered.filter(group => group.id !== "inactive");
16✔
626

627
    if (!props.showInactive) {
16!
628
      filtered.push({id: "inactive", value: false});
16✔
629
    }
630
    return {filtered};
16✔
631
  }
632

633
  onFilteredChange = filtered => {
8✔
634
    this.setState({filtered});
×
635
  };
636

637
  render() {
638
    return (
16✔
639
      <CheckboxTable
640
        ref={r => (this.checkboxTable = r)}
32✔
641
        data={this.props.groups}
642
        columns={this.state.columns}
643
        defaultSorted={[
644
          {
645
            id: "group_name",
646
          },
647
        ]}
648
        loading={this.props.loading}
649
        filterable
650
        filtered={this.state.filtered}
651
        onFilteredChange={this.onFilteredChange}
652
        {...this.props.getCheckboxProps()}
653
      />
654
    );
655
  }
656
}
657

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

732
  static getDerivedStateFromProps(props, state) {
733
    let filtered = [];
16✔
734
    for (let i = 0; i < state.filtered.length; i++) {
16✔
735
      if (state.filtered[i].id !== "hidden") {
8!
736
        filtered.push(state.filtered[i]);
×
737
      }
738
    }
739
    if (!props.showHidden) {
16!
740
      filtered.push({id: "hidden", value: false});
16✔
741
    }
742
    return {filtered};
16✔
743
  }
744

745
  onFilteredChange = filtered => {
8✔
746
    this.setState({filtered});
×
747
  };
748

749
  render() {
750
    return (
16✔
751
      <CheckboxTable
752
        ref={r => (this.checkboxTable = r)}
32✔
753
        data={this.props.students}
754
        columns={this.state.columns}
755
        defaultSorted={[
756
          {
757
            id: "user_name",
758
          },
759
        ]}
760
        loading={this.props.loading}
761
        filterable
762
        filtered={this.state.filtered}
763
        onFilteredChange={this.onFilteredChange}
764
        {...this.props.getCheckboxProps()}
765
      />
766
    );
767
  }
768
}
769

770
const GroupsTable = withSelection(RawGroupsTable);
1✔
771
const StudentsTable = withSelection(RawStudentsTable);
1✔
772

773
class GroupsActionBox extends React.Component {
774
  render = () => {
8✔
775
    var showHiddenTooltip = null;
16✔
776
    if (this.props.hiddenStudentsCount !== null && this.props.hiddenGroupsCount !== null) {
16✔
777
      showHiddenTooltip = `${I18n.t("activerecord.attributes.grouping.inactive_students", {
8✔
778
        count: this.props.hiddenStudentsCount,
779
      })}, ${I18n.t("activerecord.attributes.grouping.inactive_groups", {
780
        count: this.props.hiddenGroupsCount,
781
      })}`;
782
    }
783

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

836
export function makeGroupsManager(elem, props) {
837
  const root = createRoot(elem);
×
838
  const component = React.createRef();
×
839
  root.render(<GroupsManager {...props} ref={component} />);
×
840
  return component;
×
841
}
842

843
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