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

MarkUsProject / Markus / 17109290822

20 Aug 2025 08:16PM UTC coverage: 91.797% (-0.09%) from 91.884%
17109290822

Pull #7602

github

web-flow
Merge a552a9ddb into 899c1431b
Pull Request #7602: Added loading icon for instructor table

699 of 1486 branches covered (47.04%)

Branch coverage included in aggregate %.

10 of 14 new or added lines in 5 files covered. (71.43%)

107 existing lines in 7 files now uncovered.

42140 of 45181 relevant lines covered (93.27%)

118.79 hits per line

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

19.28
/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(props) {
10
  const {loading} = props;
2✔
11

12
  if (loading) {
2!
13
    return (
2✔
14
      <div
15
        className="flex gap-4"
16
        style={{
17
          display: "flex",
18
          justifyContent: "center",
19
          alignItems: "center",
20
          height: "50px",
21
        }}
22
        data-testid="loading-spinner"
23
      >
24
        <Grid
25
          visible={true}
26
          height="25"
27
          width="25"
28
          color="#31649B"
29
          aria-label="grid-loading"
30
          radius="12.5"
31
          wrapperStyle={{}}
32
          wrapperClass="grid-wrapper"
33
        />
34
      </div>
35
    );
36
  }
37

NEW
38
  return null;
×
39
}
40

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

68
/**
69
 * Case insensitive, locale aware, string filter function
70
 */
71
export function stringFilterMethod(filter, row) {
72
  return String(row[filter.id])
×
73
    .toLocaleLowerCase()
74
    .includes(String(filter.value).toLocaleLowerCase());
75
}
76

77
export function dateSort(a, b) {
78
  /** Sort values as dates */
79
  if (!a && !b) {
×
80
    return 0;
×
81
  } else if (!a) {
×
82
    return -1;
×
83
  } else if (!b) {
×
84
    return 1;
×
85
  } else {
86
    let a_date = new Date(a);
×
87
    let b_date = new Date(b);
×
88
    return (a_date || 0) - (b_date || 0);
×
89
  }
90
}
91

92
export function durationSort(a, b) {
93
  /** Sort values as a duration in weeks, days, hours, etc. */
94
  a = [a.weeks || -1, a.days || -1, a.hours || -1, a.minutes || -1, a.seconds || -1];
×
95
  b = [b.weeks || -1, b.days || -1, b.hours || -1, b.minutes || -1, b.seconds || -1];
×
96
  if (a < b) {
×
97
    return 1;
×
98
  } else if (b < a) {
×
99
    return -1;
×
100
  } else {
101
    return 0;
×
102
  }
103
}
104

105
/**
106
 * Text-based search filter. Based on react-table's default search filter,
107
 * with an additional aria-label attribute.
108
 */
109
export function textFilter({filter, onChange, column}) {
110
  return (
×
111
    <input
112
      type="text"
113
      style={{
114
        width: "100%",
115
      }}
116
      placeholder={column.Placeholder}
117
      value={filter ? filter.value : ""}
×
118
      aria-label={`${I18n.t("search")} ${column.Header || ""}`}
×
119
      onChange={event => onChange(event.target.value)}
×
120
    />
121
  );
122
}
123

124
/**
125
 * Select-based search filter. Options are generated from the custom column attribute
126
 * filterOptions, which is a list of objects with keys "value" and "text".
127
 * A default "all" option is prepended to the list of options; the text can be
128
 * customized by setting the filterAllOptionText column attribute.
129
 */
130
export function selectFilter({filter, onChange, column}) {
131
  let options = (column.filterOptions || []).map(({value, text}) => (
112!
132
    <option value={value} key={value}>
203✔
133
      {text}
134
    </option>
135
  ));
136
  let allOptionText = column.filterAllOptionText || I18n.t("all");
112✔
137
  options.unshift(
112✔
138
    <option value="all" key="all">
139
      {allOptionText}
140
    </option>
141
  );
142

143
  return (
112✔
144
    <select
145
      onChange={event => onChange(event.target.value)}
×
146
      style={{width: "100%"}}
147
      value={filter ? filter.value : "all"}
112!
148
      aria-label={I18n.t("filter_by", {name: column.Header})}
149
    >
150
      {options}
151
    </select>
152
  );
153
}
154

155
export function markingStateColumn(marking_states, markingStateFilter, ...override_keys) {
156
  return {
11✔
157
    Header: I18n.t("activerecord.attributes.result.marking_state"),
158
    accessor: "marking_state",
159
    Cell: row => {
160
      let marking_state = "";
13✔
161
      switch (row.original.marking_state) {
13!
162
        case "not_collected":
163
          marking_state = I18n.t("submissions.state.not_collected");
×
164
          break;
×
165
        case "incomplete":
166
          marking_state = I18n.t("submissions.state.in_progress");
×
167
          break;
×
168
        case "complete":
169
          marking_state = I18n.t("submissions.state.complete");
×
170
          break;
×
171
        case "released":
172
          marking_state = I18n.t("submissions.state.released");
13✔
173
          break;
13✔
174
        case "remark":
175
          marking_state = I18n.t("submissions.state.remark_requested");
×
176
          break;
×
177
        case "before_due_date":
178
          marking_state = I18n.t("submissions.state.before_due_date");
×
179
          break;
×
180
        default:
181
          // should not get here
182
          marking_state = row.original.marking_state;
×
183
      }
184
      return marking_state;
13✔
185
    },
186
    filterMethod: (filter, row) => {
187
      if (filter.value === "all") {
×
188
        return true;
×
189
      } else {
190
        return filter.value === row[filter.id];
×
191
      }
192
    },
193
    filterAllOptionText:
194
      I18n.t("all") +
195
      (markingStateFilter === "all"
11!
196
        ? ` (${Object.values(marking_states).reduce((a, b) => a + b)})`
55✔
197
        : ""),
198
    filterOptions: [
199
      {
200
        value: "before_due_date",
201
        text:
202
          I18n.t("submissions.state.before_due_date") +
203
          (["before_due_date", "all"].includes(markingStateFilter)
11!
204
            ? ` (${marking_states["before_due_date"]})`
205
            : ""),
206
      },
207
      {
208
        value: "not_collected",
209
        text:
210
          I18n.t("submissions.state.not_collected") +
211
          (["not_collected", "all"].includes(markingStateFilter)
11!
212
            ? ` (${marking_states["not_collected"]})`
213
            : ""),
214
      },
215
      {
216
        value: "incomplete",
217
        text:
218
          I18n.t("submissions.state.in_progress") +
219
          (["incomplete", "all"].includes(markingStateFilter)
11!
220
            ? ` (${marking_states["incomplete"]})`
221
            : ""),
222
      },
223
      {
224
        value: "complete",
225
        text:
226
          I18n.t("submissions.state.complete") +
227
          (["complete", "all"].includes(markingStateFilter)
11!
228
            ? ` (${marking_states["complete"]})`
229
            : ""),
230
      },
231
      {
232
        value: "released",
233
        text:
234
          I18n.t("submissions.state.released") +
235
          (["released", "all"].includes(markingStateFilter)
11!
236
            ? ` (${marking_states["released"]})`
237
            : ""),
238
      },
239
      {
240
        value: "remark",
241
        text:
242
          I18n.t("submissions.state.remark_requested") +
243
          (["remark", "all"].includes(markingStateFilter) ? ` (${marking_states["remark"]})` : ""),
11!
244
      },
245
    ],
246
    Filter: selectFilter,
247
    ...override_keys,
248
  };
249
}
250

251
export function getMarkingStates(data) {
252
  const markingStates = {
24✔
253
    not_collected: 0,
254
    incomplete: 0,
255
    complete: 0,
256
    released: 0,
257
    remark: 0,
258
    before_due_date: 0,
259
  };
260
  data.forEach(row => {
24✔
261
    markingStates[row["marking_state"]] += 1;
20✔
262
  });
263
  return markingStates;
24✔
264
}
265

266
export function customNoDataComponent({children, loading}) {
NEW
267
  if (loading) {
×
NEW
268
    return null;
×
269
  }
UNCOV
270
  return <p className="rt-no-data">{children}</p>;
×
271
}
272

273
export function customNoDataProps({state}) {
NEW
274
  return {loading: state.loading, data: state.data};
×
275
}
276

277
// export function customNoDataText(props) {
278
//   const {loading} = props;
279
//   if (loading) {
280
//     return "";
281
//   }
282
//   return I18n.t("students.empty_table");
283
// }
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