@@ -22,9 +22,10 @@ import { createUserNameComparator } from '../../helpers/users';
2222import { compareAssignments , compareShadowAssignments } from '../../helpers/assignments' ;
2323import { downloadString } from '../../../redux/helpers/api/download' ;
2424import Button from '../../widgets/TheButton' ;
25- import { DownloadIcon , LoadingIcon } from '../../icons' ;
25+ import Icon , { DownloadIcon , LoadingIcon } from '../../icons' ;
2626import { safeGet , EMPTY_ARRAY , EMPTY_OBJ , hasPermissions } from '../../../helpers/common' ;
2727import withLinks from '../../../helpers/withLinks' ;
28+ import { storageGetItem , storageSetItem } from '../../../helpers/localStorage' ;
2829
2930import styles from './ResultsTable.less' ;
3031import 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+
129132class 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' ,
0 commit comments