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

MarkUsProject / Markus / 27581324586

15 Jun 2026 10:44PM UTC coverage: 90.162% (-0.05%) from 90.209%
27581324586

Pull #8002

github

web-flow
Merge 78ba3e295 into 7e6b03927
Pull Request #8002: Migrate graders_manager.jsx to React Table V8

1078 of 2310 branches covered (46.67%)

Branch coverage included in aggregate %.

47 of 71 new or added lines in 3 files covered. (66.2%)

4 existing lines in 2 files now uncovered.

46357 of 50301 relevant lines covered (92.16%)

126.48 hits per line

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

33.94
/app/javascript/Components/Helpers/table_helpers.jsx
1
import React from "react";
2
import {Grid} from "react-loader-spinner";
3

4
/**
5
 * @file
6
 * Provides generic helper functions and components for react-table tables.
7
 */
8

9
export function customLoadingProp({loading}) {
10
  if (loading) {
193✔
11
    return (
42✔
12
      <div className="loading-spinner" data-testid="loading-spinner">
13
        <Grid
14
          visible={true}
15
          height="25"
16
          width="25"
17
          color="#31649B"
18
          aria-label="grid-loading"
19
          radius="12.5"
20
          wrapperStyle={{}}
21
          wrapperClass="grid-wrapper"
22
        />
23
      </div>
24
    );
25
  }
26

27
  return null;
151✔
28
}
29

30
export function defaultSort(a, b) {
31
  // Sort values, putting undefined/nulls below all other values.
32
  // Based on react-table v6 defaultSortMethod (https://github.com/tannerlinsley/react-table/tree/v6/),
33
  // but not string-based.
34
  if ((a === undefined || a === null) && (b === undefined || b === null)) {
×
35
    return 0;
×
36
  } else if (a === undefined || a === null) {
×
37
    return -1;
×
38
  } else if (b === undefined || b === null) {
×
39
    return 1;
×
40
  } else {
41
    // force any string values to lowercase
42
    a = typeof a === "string" ? a.toLowerCase() : a;
×
43
    b = typeof b === "string" ? b.toLowerCase() : b;
×
44
    // Return either 1 or -1 to indicate a sort priority
45
    if (a > b) {
×
46
      return 1;
×
47
    }
48
    if (a < b) {
×
49
      return -1;
×
50
    }
51
    // returning 0, undefined or any falsey value will use subsequent sorts or
52
    // the index as a tiebreaker
53
    return 0;
×
54
  }
55
}
56

57
/**
58
 * Case insensitive, locale aware, string filter function
59
 */
60
export function stringFilterMethod(filter, row) {
61
  return String(row[filter.id])
×
62
    .toLocaleLowerCase()
63
    .includes(String(filter.value).toLocaleLowerCase());
64
}
65

66
export function dateSort(a, b) {
67
  /** Sort values as dates */
68
  if (!a && !b) {
×
69
    return 0;
×
70
  } else if (!a) {
×
71
    return -1;
×
72
  } else if (!b) {
×
73
    return 1;
×
74
  } else {
75
    let a_date = new Date(a);
×
76
    let b_date = new Date(b);
×
77
    return (a_date || 0) - (b_date || 0);
×
78
  }
79
}
80

81
export function durationSort(a, b) {
82
  /** Sort values as a duration in weeks, days, hours, etc. */
83
  a = [a.weeks || -1, a.days || -1, a.hours || -1, a.minutes || -1, a.seconds || -1];
×
84
  b = [b.weeks || -1, b.days || -1, b.hours || -1, b.minutes || -1, b.seconds || -1];
×
85
  if (a < b) {
×
86
    return 1;
×
87
  } else if (b < a) {
×
88
    return -1;
×
89
  } else {
90
    return 0;
×
91
  }
92
}
93

94
/**
95
 * Text-based search filter. Based on react-table's default search filter,
96
 * with an additional aria-label attribute.
97
 */
98
export function textFilter({filter, onChange, column}) {
UNCOV
99
  return (
×
100
    <input
101
      type="text"
102
      style={{
103
        width: "100%",
104
      }}
105
      placeholder={column.Placeholder}
106
      value={filter ? filter.value : ""}
×
107
      aria-label={`${I18n.t("search")} ${column.Header || ""}`}
×
UNCOV
108
      onChange={event => onChange(event.target.value)}
×
109
    />
110
  );
111
}
112

113
/**
114
 * Locale-aware substring match with optional case sensitivity. null/undefined
115
 * arguments are treated as the empty string, so callers do not have to guard
116
 * against them.
117
 */
118
export function caseSensitiveIncludes(haystack, needle, caseSensitive) {
119
  const a = haystack === null || haystack === undefined ? "" : String(haystack);
69!
120
  const b = needle === null || needle === undefined ? "" : String(needle);
69!
121
  if (caseSensitive) return a.includes(b);
69✔
122
  return a.toLocaleLowerCase().includes(b.toLocaleLowerCase());
44✔
123
}
124

125
/**
126
 * filterMethod that matches `row[filter.id]` against the filter's search text
127
 * with the filter's own case sensitivity. Pair with `caseSensitiveTextFilter`,
128
 * which stores `{filterValue, caseSensitive}` as the filter value. Empty
129
 * filters match every row.
130
 */
131
export function caseSensitiveStringFilterMethod(filter, row) {
132
  const {filterValue, caseSensitive} = filter.value;
28✔
133
  if (!filterValue) return true;
28✔
134
  return caseSensitiveIncludes(row[filter.id], filterValue, caseSensitive);
16✔
135
}
136

137
/**
138
 * A Filter component pairing a text input with an "Aa" checkbox toggle for
139
 * case-sensitive matching. It owns both the search text and the toggle,
140
 * passing them to the table together via `onChange` as
141
 * `{filterValue, caseSensitive}`. Matching defaults to case-insensitive. Pair
142
 * with `caseSensitiveStringFilterMethod`, or a custom filterMethod that reads
143
 * `filter.value.filterValue` and `filter.value.caseSensitive`.
144
 */
145
export function caseSensitiveTextFilter({filter, onChange, column}) {
146
  const filterValue = filter ? filter.value.filterValue : "";
110✔
147
  const caseSensitive = filter ? filter.value.caseSensitive : false;
110✔
148

149
  return (
110✔
150
    <div style={{display: "flex", alignItems: "center", gap: "4px"}}>
151
      <input
152
        type="text"
153
        style={{flex: 1, minWidth: 0}}
154
        value={filterValue}
155
        aria-label={`${I18n.t("search")} ${column.Header || ""}`}
110!
156
        onChange={event => onChange({filterValue: event.target.value, caseSensitive})}
9✔
157
      />
158
      <label
159
        title={I18n.t("table.case_sensitive_search")}
160
        style={{display: "flex", alignItems: "center", cursor: "pointer"}}
161
      >
162
        <input
163
          type="checkbox"
164
          checked={caseSensitive}
165
          onChange={event => onChange({filterValue, caseSensitive: event.target.checked})}
7✔
166
          aria-label={I18n.t("table.case_sensitive_search")}
167
          data-testid={`${column.id}_case_sensitive`}
168
        />
169
        <span style={{fontSize: "1.05em", marginLeft: "2px"}}>
170
          {I18n.t("table.case_sensitive_indicator")}
171
        </span>
172
      </label>
173
    </div>
174
  );
175
}
176

177
/**
178
 * Select-based search filter. Options are generated from the custom column attribute
179
 * filterOptions, which is a list of objects with keys "value" and "text".
180
 * A default "all" option is prepended to the list of options; the text can be
181
 * customized by setting the filterAllOptionText column attribute.
182
 */
183
export function selectFilter({filter, onChange, column}) {
184
  let options = (column.filterOptions || []).map(({value, text}) => (
294!
185
    <option value={value} key={value}>
762✔
186
      {text}
187
    </option>
188
  ));
189
  let allOptionText = column.filterAllOptionText || I18n.t("all");
294✔
190
  options.unshift(
294✔
191
    <option value="all" key="all">
192
      {allOptionText}
193
    </option>
194
  );
195

196
  return (
294✔
197
    <select
198
      onChange={event => onChange(event.target.value)}
×
199
      style={{width: "100%"}}
200
      value={filter ? filter.value : "all"}
294!
201
      aria-label={I18n.t("filter_by", {name: column.Header})}
202
    >
203
      {options}
204
    </select>
205
  );
206
}
207

208
export function markingStateColumn(marking_states, markingStateFilter, ...override_keys) {
209
  return {
20✔
210
    Header: I18n.t("activerecord.attributes.result.marking_state"),
211
    accessor: "marking_state",
212
    Cell: row => {
213
      let marking_state = "";
64✔
214
      switch (row.original.marking_state) {
64!
215
        case "not_collected":
216
          marking_state = I18n.t("submissions.state.not_collected");
×
217
          break;
×
218
        case "incomplete":
219
          marking_state = I18n.t("submissions.state.in_progress");
×
220
          break;
×
221
        case "complete":
222
          marking_state = I18n.t("submissions.state.complete");
×
223
          break;
×
224
        case "released":
225
          marking_state = I18n.t("submissions.state.released");
64✔
226
          break;
64✔
227
        case "remark":
228
          marking_state = I18n.t("submissions.state.remark_requested");
×
229
          break;
×
230
        case "before_due_date":
231
          marking_state = I18n.t("submissions.state.before_due_date");
×
232
          break;
×
233
        default:
234
          // should not get here
235
          marking_state = row.original.marking_state;
×
236
      }
237
      return marking_state;
64✔
238
    },
239
    filterMethod: (filter, row) => {
240
      if (filter.value === "all") {
×
241
        return true;
×
242
      } else {
243
        return filter.value === row[filter.id];
×
244
      }
245
    },
246
    filterAllOptionText:
247
      I18n.t("all") +
248
      (markingStateFilter === "all"
20!
249
        ? ` (${Object.values(marking_states).reduce((a, b) => a + b)})`
100✔
250
        : ""),
251
    filterOptions: [
252
      {
253
        value: "before_due_date",
254
        text:
255
          I18n.t("submissions.state.before_due_date") +
256
          (["before_due_date", "all"].includes(markingStateFilter)
20!
257
            ? ` (${marking_states["before_due_date"]})`
258
            : ""),
259
      },
260
      {
261
        value: "not_collected",
262
        text:
263
          I18n.t("submissions.state.not_collected") +
264
          (["not_collected", "all"].includes(markingStateFilter)
20!
265
            ? ` (${marking_states["not_collected"]})`
266
            : ""),
267
      },
268
      {
269
        value: "incomplete",
270
        text:
271
          I18n.t("submissions.state.in_progress") +
272
          (["incomplete", "all"].includes(markingStateFilter)
20!
273
            ? ` (${marking_states["incomplete"]})`
274
            : ""),
275
      },
276
      {
277
        value: "complete",
278
        text:
279
          I18n.t("submissions.state.complete") +
280
          (["complete", "all"].includes(markingStateFilter)
20!
281
            ? ` (${marking_states["complete"]})`
282
            : ""),
283
      },
284
      {
285
        value: "released",
286
        text:
287
          I18n.t("submissions.state.released") +
288
          (["released", "all"].includes(markingStateFilter)
20!
289
            ? ` (${marking_states["released"]})`
290
            : ""),
291
      },
292
      {
293
        value: "remark",
294
        text:
295
          I18n.t("submissions.state.remark_requested") +
296
          (["remark", "all"].includes(markingStateFilter) ? ` (${marking_states["remark"]})` : ""),
20!
297
      },
298
    ],
299
    Filter: selectFilter,
300
    ...override_keys,
301
  };
302
}
303

304
export function getMarkingStates(data) {
305
  const markingStates = {
44✔
306
    not_collected: 0,
307
    incomplete: 0,
308
    complete: 0,
309
    released: 0,
310
    remark: 0,
311
    before_due_date: 0,
312
  };
313
  data.forEach(row => {
44✔
314
    markingStates[row["marking_state"]] += 1;
51✔
315
  });
316
  return markingStates;
44✔
317
}
318

319
export function customNoDataComponent({children, loading}) {
320
  if (loading) {
80!
321
    return null;
×
322
  }
323
  return <p className="rt-no-data">{children}</p>;
80✔
324
}
325

326
export function customGetNoDataProps(state) {
327
  return {loading: state.loading, data: state.data};
×
328
}
329

330
export function getTimeExtension(extension, timePeriods) {
331
  return timePeriods
52✔
332
    .map(key => {
333
      const val = extension[key];
208✔
334

335
      if (!val) {
208✔
336
        return null;
186✔
337
      }
338
      // don't build these strings dynamically or they will be missed by the i18n-tasks checkers.
339
      if (key === "weeks") {
22!
340
        return `${val} ${I18n.t("durations.weeks", {count: val})}`;
×
341
      } else if (key === "days") {
22!
342
        return `${val} ${I18n.t("durations.days", {count: val})}`;
22✔
343
      } else if (key === "hours") {
×
344
        return `${val} ${I18n.t("durations.hours", {count: val})}`;
×
345
      } else if (key === "minutes") {
×
346
        return `${val} ${I18n.t("durations.minutes", {count: val})}`;
×
347
      }
348
      return "";
×
349
    })
350
    .filter(Boolean)
351
    .join(", ");
352
}
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