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

MarkUsProject / Markus / 29673128555

19 Jul 2026 04:18AM UTC coverage: 90.37% (-0.04%) from 90.411%
29673128555

Pull #8068

github

web-flow
Merge dccb0480f into 59d4c37c6
Pull Request #8068: Groups tab tables refactor

1219 of 2484 branches covered (49.07%)

Branch coverage included in aggregate %.

38 of 78 new or added lines in 3 files covered. (48.72%)

18 existing lines in 1 file now uncovered.

47355 of 51266 relevant lines covered (92.37%)

127.93 hits per line

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

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

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

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

26
  return null;
87✔
27
}
28

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

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

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

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

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

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

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

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

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

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

201
  return (
206✔
202
    <select
UNCOV
203
      onChange={event => onChange(event.target.value)}
×
204
      style={{width: "100%"}}
205
      value={filter ? filter.value : "all"}
206!
206
      aria-label={I18n.t("filter_by", {name: column.Header})}
207
    >
208
      {options}
209
    </select>
210
  );
211
}
212

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

309
export function getMarkingStates(data) {
310
  const markingStates = {
62✔
311
    not_collected: 0,
312
    incomplete: 0,
313
    complete: 0,
314
    released: 0,
315
    remark: 0,
316
    before_due_date: 0,
317
  };
318
  data.forEach(row => {
62✔
319
    markingStates[row["marking_state"]] += 1;
67✔
320
  });
321
  return markingStates;
62✔
322
}
323

324
export function customNoDataComponent({children, loading}) {
325
  if (loading) {
44!
UNCOV
326
    return null;
×
327
  }
328
  return <p className="rt-no-data">{children}</p>;
44✔
329
}
330

331
export function customGetNoDataProps(state) {
UNCOV
332
  return {loading: state.loading, data: state.data};
×
333
}
334

335
export function getTimeExtension(extension, timePeriods) {
336
  return timePeriods
40✔
337
    .map(key => {
338
      const val = extension[key];
160✔
339

340
      if (!val) {
160✔
341
        return null;
133✔
342
      }
343
      // don't build these strings dynamically or they will be missed by the i18n-tasks checkers.
344
      if (key === "weeks") {
27!
345
        return `${val} ${I18n.t("durations.weeks", {count: val})}`;
×
346
      } else if (key === "days") {
27!
347
        return `${val} ${I18n.t("durations.days", {count: val})}`;
27✔
348
      } else if (key === "hours") {
×
UNCOV
349
        return `${val} ${I18n.t("durations.hours", {count: val})}`;
×
UNCOV
350
      } else if (key === "minutes") {
×
UNCOV
351
        return `${val} ${I18n.t("durations.minutes", {count: val})}`;
×
352
      }
UNCOV
353
      return "";
×
354
    })
355
    .filter(Boolean)
356
    .join(", ");
357
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc