Skip to content

Commit 6eb7f84

Browse files
committed
Adding icon that will allow the user hide everyone else in the results table, so one can better read his/her own points.
1 parent 164b253 commit 6eb7f84

File tree

3 files changed

+198
-127
lines changed

3 files changed

+198
-127
lines changed

src/components/Groups/ResultsTable/ResultsTable.js

Lines changed: 176 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ import { createUserNameComparator } from '../../helpers/users';
2222
import { compareAssignments, compareShadowAssignments } from '../../helpers/assignments';
2323
import { downloadString } from '../../../redux/helpers/api/download';
2424
import Button from '../../widgets/TheButton';
25-
import { DownloadIcon, LoadingIcon } from '../../icons';
25+
import Icon, { DownloadIcon, LoadingIcon } from '../../icons';
2626
import { safeGet, EMPTY_ARRAY, EMPTY_OBJ, hasPermissions } from '../../../helpers/common';
2727
import withLinks from '../../../helpers/withLinks';
28+
import { storageGetItem, storageSetItem } from '../../../helpers/localStorage';
2829

2930
import styles from './ResultsTable.less';
3031
import escapeString from '../../helpers/escapeString';
@@ -126,13 +127,26 @@ const getCSVValues = (assignments, shadowAssignments, data, locale) => {
126127
return result.map(row => row.join(SEPARATOR)).join(NEWLINE);
127128
};
128129

130+
const localStorageShowOnlyMeKey = 'ResultsTable.showOnlyMe';
131+
129132
class ResultsTable extends Component {
130133
state = {
131134
dialogOpen: false,
132135
dialogClosing: false,
133136
dialogUserId: null,
134137
dialogAssignmentId: null,
135138
dialogShadowId: null,
139+
showOnlyMe: false,
140+
};
141+
142+
componentDidMount = () => {
143+
this.setState({ showOnlyMe: storageGetItem(localStorageShowOnlyMeKey, false) });
144+
};
145+
146+
toggleShowOnlyMe = () => {
147+
const showOnlyMe = !this.state.showOnlyMe;
148+
storageSetItem(localStorageShowOnlyMeKey, showOnlyMe);
149+
this.setState({ showOnlyMe });
136150
};
137151

138152
openDialogAssignment = (dialogUserId, dialogAssignmentId) => {
@@ -204,151 +218,188 @@ class ResultsTable extends Component {
204218
}
205219
};
206220

207-
prepareColumnDescriptors = defaultMemoize((assignments, shadowAssignments, loggedUser, locale, isTeacher) => {
208-
const {
209-
group,
210-
links: {
211-
ASSIGNMENT_STATS_URI_FACTORY,
212-
ASSIGNMENT_DETAIL_URI_FACTORY,
213-
SHADOW_ASSIGNMENT_DETAIL_URI_FACTORY,
214-
GROUP_USER_SOLUTIONS_URI_FACTORY,
215-
},
216-
} = this.props;
217-
218-
const nameComparator = createUserNameComparator(locale);
219-
220-
/*
221-
* User Name (First Column)
222-
*/
223-
const columns = [
224-
new SortableTableColumnDescriptor('user', <FormattedMessage id="generic.nameOfPerson" defaultMessage="Name" />, {
225-
headerSuffix: <FormattedMessage id="app.groupResultsTable.maxPointsRow" defaultMessage="Max points:" />,
226-
headerSuffixClassName: styles.maxPointsRow,
227-
className: 'text-left',
228-
comparator: ({ user: u1 }, { user: u2 }) => nameComparator(u1, u2),
229-
cellRenderer: user =>
230-
user && (
231-
<UsersName
232-
{...user}
233-
currentUserId={loggedUser.id}
234-
showEmail="icon"
235-
showExternalIdentifiers
236-
link={
237-
isTeacher || user.id === loggedUser.id ? GROUP_USER_SOLUTIONS_URI_FACTORY(group.id, user.id) : false
238-
}
239-
/>
240-
),
241-
}),
242-
];
243-
244-
/*
245-
* Assignments
246-
*/
247-
assignments.sort(compareAssignments).forEach(assignment =>
248-
columns.push(
221+
prepareColumnDescriptors = defaultMemoize(
222+
(assignments, shadowAssignments, loggedUser, locale, isTeacher, showOnlyMe = false) => {
223+
const {
224+
group,
225+
links: {
226+
ASSIGNMENT_STATS_URI_FACTORY,
227+
ASSIGNMENT_DETAIL_URI_FACTORY,
228+
SHADOW_ASSIGNMENT_DETAIL_URI_FACTORY,
229+
GROUP_USER_SOLUTIONS_URI_FACTORY,
230+
},
231+
} = this.props;
232+
233+
const nameComparator = createUserNameComparator(locale);
234+
235+
/*
236+
* User Name (First Column)
237+
*/
238+
const columns = [
249239
new SortableTableColumnDescriptor(
250-
assignment.id,
251-
(
252-
<div className={styles.verticalText}>
253-
<div>
254-
<Link
255-
to={
256-
isTeacher
257-
? ASSIGNMENT_STATS_URI_FACTORY(assignment.id)
258-
: ASSIGNMENT_DETAIL_URI_FACTORY(assignment.id)
259-
}>
260-
<LocalizedExerciseName entity={assignment} />
261-
</Link>
262-
</div>
263-
</div>
264-
),
240+
'user',
241+
<FormattedMessage id="generic.nameOfPerson" defaultMessage="Name" />,
265242
{
266-
headerClassName: 'text-center',
267-
className: 'text-center',
268-
headerSuffix:
269-
assignment.maxPointsBeforeFirstDeadline +
270-
(assignment.maxPointsBeforeSecondDeadline ? ` / ${assignment.maxPointsBeforeSecondDeadline}` : ''),
243+
headerSuffix: <FormattedMessage id="app.groupResultsTable.maxPointsRow" defaultMessage="Max points:" />,
271244
headerSuffixClassName: styles.maxPointsRow,
272-
cellRenderer: assignmentCellRendererCreator(assignments, locale),
273-
isClickable: hasPermissions(assignment, 'viewAssignmentSolutions')
274-
? true
275-
: (userId, _) => userId === loggedUser.id,
276-
onClick: hasPermissions(assignment, 'viewAssignmentSolutions')
277-
? (userId, assignmentId) => this.openDialogAssignment(userId, assignmentId)
278-
: (userId, assignmentId) => userId === loggedUser.id && this.openDialogAssignment(userId, assignmentId),
245+
className: 'text-left',
246+
comparator: showOnlyMe ? null : ({ user: u1 }, { user: u2 }) => nameComparator(u1, u2),
247+
cellRenderer: user =>
248+
user && (
249+
<>
250+
<UsersName
251+
{...user}
252+
currentUserId={loggedUser.id}
253+
showEmail="icon"
254+
showExternalIdentifiers
255+
link={
256+
isTeacher || user.id === loggedUser.id
257+
? GROUP_USER_SOLUTIONS_URI_FACTORY(group.id, user.id)
258+
: false
259+
}
260+
/>
261+
{!isTeacher && user.id === loggedUser.id && hasPermissions(group, 'viewStats') && (
262+
<OverlayTrigger
263+
placement="bottom"
264+
overlay={
265+
<Tooltip id="onlyShowMe">
266+
{showOnlyMe ? (
267+
<FormattedMessage
268+
id="app.resultsTable.cancelOnlyShowMe"
269+
defaultMessage="Show all students in the group"
270+
/>
271+
) : (
272+
<FormattedMessage
273+
id="app.resultsTable.onlyShowMe"
274+
defaultMessage="Hide everyone except for myself"
275+
/>
276+
)}
277+
</Tooltip>
278+
}>
279+
<Icon
280+
icon={showOnlyMe ? 'users' : ['far', 'user-circle']}
281+
largeGapLeft
282+
className="text-success"
283+
onClick={this.toggleShowOnlyMe}
284+
/>
285+
</OverlayTrigger>
286+
)}
287+
</>
288+
),
279289
}
290+
),
291+
];
292+
293+
/*
294+
* Assignments
295+
*/
296+
assignments.sort(compareAssignments).forEach(assignment =>
297+
columns.push(
298+
new SortableTableColumnDescriptor(
299+
assignment.id,
300+
(
301+
<div className={styles.verticalText}>
302+
<div>
303+
<Link
304+
to={
305+
isTeacher
306+
? ASSIGNMENT_STATS_URI_FACTORY(assignment.id)
307+
: ASSIGNMENT_DETAIL_URI_FACTORY(assignment.id)
308+
}>
309+
<LocalizedExerciseName entity={assignment} />
310+
</Link>
311+
</div>
312+
</div>
313+
),
314+
{
315+
headerClassName: 'text-center',
316+
className: 'text-center',
317+
headerSuffix:
318+
assignment.maxPointsBeforeFirstDeadline +
319+
(assignment.maxPointsBeforeSecondDeadline ? ` / ${assignment.maxPointsBeforeSecondDeadline}` : ''),
320+
headerSuffixClassName: styles.maxPointsRow,
321+
cellRenderer: assignmentCellRendererCreator(assignments, locale),
322+
isClickable: hasPermissions(assignment, 'viewAssignmentSolutions')
323+
? true
324+
: (userId, _) => userId === loggedUser.id,
325+
onClick: hasPermissions(assignment, 'viewAssignmentSolutions')
326+
? (userId, assignmentId) => this.openDialogAssignment(userId, assignmentId)
327+
: (userId, assignmentId) => userId === loggedUser.id && this.openDialogAssignment(userId, assignmentId),
328+
}
329+
)
280330
)
281-
)
282-
);
331+
);
332+
333+
/*
334+
* Shadow Assignments
335+
*/
336+
shadowAssignments.sort(compareShadowAssignments).forEach(shadowAssignment =>
337+
columns.push(
338+
new SortableTableColumnDescriptor(
339+
shadowAssignment.id,
340+
(
341+
<div className={styles.verticalText}>
342+
<div>
343+
<Link to={SHADOW_ASSIGNMENT_DETAIL_URI_FACTORY(shadowAssignment.id)}>
344+
<LocalizedExerciseName entity={shadowAssignment} />
345+
</Link>
346+
</div>
347+
</div>
348+
),
349+
{
350+
className: 'text-center',
351+
headerSuffix: shadowAssignment.maxPoints,
352+
headerSuffixClassName: styles.maxPointsRow,
353+
cellRenderer: shadowAssignmentCellRendererCreator(shadowAssignments, locale),
354+
isClickable: true,
355+
onClick: (userId, shadowId) => this.openDialogShadowAssignment(userId, shadowId),
356+
}
357+
)
358+
)
359+
);
283360

284-
/*
285-
* Shadow Assignments
286-
*/
287-
shadowAssignments.sort(compareShadowAssignments).forEach(shadowAssignment =>
361+
/*
362+
* Total points and optionally buttons
363+
*/
288364
columns.push(
289365
new SortableTableColumnDescriptor(
290-
shadowAssignment.id,
291-
(
292-
<div className={styles.verticalText}>
293-
<div>
294-
<Link to={SHADOW_ASSIGNMENT_DETAIL_URI_FACTORY(shadowAssignment.id)}>
295-
<LocalizedExerciseName entity={shadowAssignment} />
296-
</Link>
297-
</div>
298-
</div>
299-
),
366+
'total',
367+
<FormattedMessage id="app.resultsTable.total" defaultMessage="Total" />,
300368
{
301369
className: 'text-center',
302-
headerSuffix: shadowAssignment.maxPoints,
303370
headerSuffixClassName: styles.maxPointsRow,
304-
cellRenderer: shadowAssignmentCellRendererCreator(shadowAssignments, locale),
305-
isClickable: true,
306-
onClick: (userId, shadowId) => this.openDialogShadowAssignment(userId, shadowId),
371+
comparator: ({ total: t1, user: u1 }, { total: t2, user: u2 }) =>
372+
(Number(t2 && t2.gained) || -1) - (Number(t1 && t1.gained) || -1) || nameComparator(u1, u2),
373+
cellRenderer: points => <strong>{points ? `${points.gained}/${points.total}` : '-/-'}</strong>,
307374
}
308375
)
309-
)
310-
);
376+
);
311377

312-
/*
313-
* Total points and optionally buttons
314-
*/
315-
columns.push(
316-
new SortableTableColumnDescriptor(
317-
'total',
318-
<FormattedMessage id="app.resultsTable.total" defaultMessage="Total" />,
319-
{
320-
className: 'text-center',
321-
headerSuffixClassName: styles.maxPointsRow,
322-
comparator: ({ total: t1, user: u1 }, { total: t2, user: u2 }) =>
323-
(Number(t2 && t2.gained) || -1) - (Number(t1 && t1.gained) || -1) || nameComparator(u1, u2),
324-
cellRenderer: points => <strong>{points ? `${points.gained}/${points.total}` : '-/-'}</strong>,
325-
}
326-
)
327-
);
378+
if (hasPermissions(group, 'update')) {
379+
columns.push(
380+
new SortableTableColumnDescriptor('buttons', '', {
381+
headerSuffixClassName: styles.maxPointsRow,
382+
className: 'text-right',
383+
})
384+
);
385+
}
328386

329-
if (hasPermissions(group, 'update')) {
330-
columns.push(
331-
new SortableTableColumnDescriptor('buttons', '', {
332-
headerSuffixClassName: styles.maxPointsRow,
333-
className: 'text-right',
334-
})
335-
);
387+
return columns;
336388
}
337-
338-
return columns;
339-
});
389+
);
340390

341391
// Re-format the data, so they can be rendered by the SortableTable ...
342-
prepareData = defaultMemoize((assignments, shadowAssignments, users, stats, showButtons) => {
392+
prepareData = defaultMemoize((assignments, shadowAssignments, users, stats, showOnlyMe = false) => {
343393
const { loggedUser, renderActions, group } = this.props;
344-
if (!hasPermissions(group, 'viewStats')) {
394+
if (!hasPermissions(group, 'viewStats') || showOnlyMe) {
345395
users = users.filter(({ id }) => id === loggedUser.id);
346396
}
347397

348398
return users.map(user => {
349399
const userStats = stats.find(stat => stat.userId === user.id);
350400
const data = {
351401
id: user.id,
402+
selected: !showOnlyMe && user.id === loggedUser.id,
352403
user: user,
353404
total: userStats && userStats.points,
354405
buttons: renderActions && hasPermissions(group, 'update') ? renderActions(user.id) : '',
@@ -400,10 +451,11 @@ class ResultsTable extends Component {
400451
shadowAssignments,
401452
loggedUser,
402453
locale,
403-
isAdmin || isSupervisor || isObserver
454+
isAdmin || isSupervisor || isObserver,
455+
this.state.showOnlyMe
404456
)}
405457
defaultOrder="user"
406-
data={this.prepareData(assignments, shadowAssignments, users, stats)}
458+
data={this.prepareData(assignments, shadowAssignments, users, stats, this.state.showOnlyMe)}
407459
empty={
408460
<div className="text-center text-muted">
409461
<FormattedMessage
@@ -412,6 +464,7 @@ class ResultsTable extends Component {
412464
/>
413465
</div>
414466
}
467+
className="hover mb-0"
415468
/>
416469
{(isAdmin || isSupervisor || isObserver) && (
417470
<div className="text-center">
@@ -424,7 +477,7 @@ class ResultsTable extends Component {
424477
getCSVValues(
425478
assignments,
426479
shadowAssignments,
427-
this.prepareData(assignments, shadowAssignments, users, stats),
480+
this.prepareData(assignments, shadowAssignments, users, stats, this.state.showOnlyMe),
428481
locale
429482
),
430483
'text/csv;charset=utf-8',

src/components/widgets/SortableTable/SortableTable.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class SortableTable extends Component {
1313

1414
// Default row rendering fucntion (if the user does not provide custom function)
1515
defaultRowRenderer = (row, idx, columns) => (
16-
<tr key={row.id || idx}>
16+
<tr key={row.id || idx} className={row.selected ? 'selected' : ''}>
1717
{columns.map(({ id: colId, cellRenderer, style, className, onClick, isClickable }) => {
1818
if (typeof isClickable === 'function') {
1919
isClickable = isClickable(row.id, colId);

0 commit comments

Comments
 (0)