11import React , { Component } from 'react' ;
22import PropTypes from 'prop-types' ;
3- import { FormattedMessage } from 'react-intl' ;
3+ import { FormattedMessage , FormattedNumber } from 'react-intl' ;
44import { Link } from 'react-router-dom' ;
5+ import { Table , Badge , OverlayTrigger , Tooltip } from 'react-bootstrap' ;
6+ import { lruMemoize } from 'reselect' ;
57
68import GroupsNameContainer from '../../../containers/GroupsNameContainer' ;
79import UsersNameContainer from '../../../containers/UsersNameContainer' ;
810import AssignmentNameContainer from '../../../containers/AssignmentNameContainer' ;
911import DateTime from '../../widgets/DateTime' ;
10- import Button from '../../widgets/TheButton' ;
12+ import Button , { TheButtonGroup } from '../../widgets/TheButton' ;
1113import Box from '../../widgets/Box' ;
12- import Icon , { AssignmentIcon , GroupIcon , LoadingIcon , RefreshIcon , WarningIcon } from '../../icons' ;
14+ import EnvironmentsListItem from '../../helpers/EnvironmentsList/EnvironmentsListItem.js' ;
15+ import Points from '../../Assignments/SolutionsTable/Points.js' ;
16+ import Icon , {
17+ AssignmentIcon ,
18+ CodeFileIcon ,
19+ DetailIcon ,
20+ GroupIcon ,
21+ LoadingIcon ,
22+ RefreshIcon ,
23+ ReviewIcon ,
24+ WarningIcon ,
25+ } from '../../icons' ;
1326import { resourceStatus } from '../../../redux/helpers/resourceManager' ;
27+ import { objectMap } from '../../../helpers/common.js' ;
1428import withLinks from '../../../helpers/withLinks.js' ;
1529
30+ const getGroupsCount = lruMemoize (
31+ groups => groups && objectMap ( groups , g => Object . values ( g ) . reduce ( ( count , solutions ) => count + solutions . length , 0 ) )
32+ ) ;
33+
1634class PendingReviewsList extends Component {
1735 state = { allPending : false } ;
1836
37+ toggleGroupOpened = id => this . setState ( { [ `group-${ id } ` ] : ! this . state [ `group-${ id } ` ] } ) ;
38+
1939 closeAll = ( ) => {
2040 const { solutions, closeReview, refresh } = this . props ;
2141
@@ -40,12 +60,15 @@ class PendingReviewsList extends Component {
4060 const {
4161 solutionsState = resourceStatus . PENDING ,
4262 solutions = { } ,
63+ runtimeEnvironments,
4364 updatingSelector,
4465 closeReview = null ,
4566 refresh = null ,
46- links : { SOLUTION_SOURCE_CODES_URI_FACTORY } ,
67+ links : { SOLUTION_DETAIL_URI_FACTORY , SOLUTION_SOURCE_CODES_URI_FACTORY , GROUP_USER_SOLUTIONS_URI_FACTORY } ,
4768 } = this . props ;
4869
70+ const groupCounts = getGroupsCount ( solutions ) ;
71+
4972 return solutionsState === resourceStatus . FULFILLED && ( ! solutions || Object . keys ( solutions ) . length === 0 ) ? null : (
5073 < Box
5174 title = { < FormattedMessage id = "app.pendingReviewsList.title" defaultMessage = "All open reviews" /> }
@@ -82,68 +105,165 @@ class PendingReviewsList extends Component {
82105 </ div >
83106 ) }
84107
85- { solutionsState === resourceStatus . FULFILLED &&
86- Object . keys ( solutions ) . map ( groupId => (
87- < div key = { groupId } className = "m-3" >
88- < GroupIcon className = "text-muted" gapRight />
89- < GroupsNameContainer groupId = { groupId } fullName translations links />
90-
91- { Object . keys ( solutions [ groupId ] ) . map ( assignmentId => (
92- < div key = { assignmentId } className = "ml-4 my-1" >
93- < AssignmentIcon className = "text-muted" gapRight />
94- < AssignmentNameContainer assignmentId = { assignmentId } />
95-
96- { solutions [ groupId ] [ assignmentId ] . map ( ( solution , idx ) => (
97- < div key = { solution ? solution . id : `loading-${ idx } ` } className = "ml-4" >
98- { solution ? (
99- < >
100- < Link to = { SOLUTION_SOURCE_CODES_URI_FACTORY ( assignmentId , solution . id ) } >
101- < span >
102- < UsersNameContainer userId = { solution . authorId } isSimple noAutoload />
103- </ span >
104- #{ solution . attemptIndex } { ' ' }
105- < span className = "px-1 text-muted" >
106- < FormattedMessage id = "app.pendingReviewsList.submitted" defaultMessage = "submitted" />
107- </ span > { ' ' }
108- < DateTime unixts = { solution . createdAt } showTime = { false } /> (
109- < DateTime unixts = { solution . createdAt } showDate = { false } /> ){ ' ' }
110- </ Link >
111- < span className = "px-1 text-muted" >
112- < FormattedMessage
113- id = "app.pendingReviewsList.reviewOpenedAt"
114- defaultMessage = "review opened at"
115- /> { ' ' }
116- </ span >
117- < DateTime unixts = { solution . review . startedAt } showTime = { false } /> (
118- < DateTime unixts = { solution . review . startedAt } showDate = { false } /> )
119- { closeReview && (
120- < Button
121- size = "xs"
122- variant = "success"
123- className = "ml-3"
124- disabled = { updatingSelector ( solution . id ) }
125- onClick = { ( ) => closeReview ( { groupId, assignmentId, solutionId : solution . id } ) } >
126- { updatingSelector ( solution . id ) ? (
127- < LoadingIcon gapRight />
108+ { solutionsState === resourceStatus . FULFILLED && (
109+ < Table hover className = "mb-0" >
110+ { Object . keys ( solutions ) . map ( groupId => (
111+ < tbody key = { groupId } >
112+ < tr className = "bg-light" >
113+ < td className = "shrink-col" >
114+ < GroupIcon className = "text-muted" />
115+ </ td >
116+ < td colSpan = { 8 } >
117+ < GroupsNameContainer groupId = { groupId } fullName translations links />
118+ </ td >
119+ < td className = "text-right text-muted" >
120+ { this . state [ `group-${ groupId } ` ] && (
121+ < Badge variant = "primary" pill className = "px-2 mr-3" >
122+ { groupCounts ?. [ groupId ] }
123+ </ Badge >
124+ ) }
125+ < Icon
126+ className = "valign-middle"
127+ icon = { ! this . state [ `group-${ groupId } ` ] ? 'circle-chevron-down' : 'circle-chevron-left' }
128+ gapRight
129+ timid
130+ onClick = { ( ) => this . toggleGroupOpened ( groupId ) }
131+ />
132+ </ td >
133+ </ tr >
134+
135+ { ! this . state [ `group-${ groupId } ` ] &&
136+ Object . keys ( solutions [ groupId ] ) . map ( assignmentId =>
137+ solutions [ groupId ] [ assignmentId ] . map ( ( solution , idx ) => (
138+ < tr key = { solution ? solution . id : `loading-${ idx } ` } className = "ml-4" >
139+ < td className = "shrink-col" > </ td >
140+ < td className = "shrink-col" >
141+ < AssignmentIcon className = "text-muted" />
142+ </ td >
143+ < td >
144+ < AssignmentNameContainer assignmentId = { assignmentId } solutionsLink />
145+ </ td >
146+
147+ { solution ? (
148+ < >
149+ < td >
150+ < UsersNameContainer
151+ userId = { solution . authorId }
152+ noAutoload
153+ link = { GROUP_USER_SOLUTIONS_URI_FACTORY ( groupId , solution . authorId ) }
154+ />
155+ </ td >
156+
157+ < td >
158+ < strong > #{ solution . attemptIndex } </ strong >
159+ < small className = "ml-2 text-muted" >
160+ (< DateTime unixts = { solution . createdAt } /> )
161+ </ small >
162+ </ td >
163+
164+ < td className = "text-center text-nowrap valign-middle" >
165+ { solution . lastSubmission . evaluation ? (
166+ < strong className = "text-success" >
167+ < FormattedNumber style = "percent" value = { solution . lastSubmission . evaluation . score } />
168+ </ strong >
128169 ) : (
129- < Icon icon = "boxes-packing" gapRight / >
170+ < span className = "text-danger" > - </ span >
130171 ) }
131- < FormattedMessage
132- id = "app.solution.actions.review.close"
133- defaultMessage = "Close Review"
172+ </ td >
173+
174+ < td className = "text-center text-nowrap valign-middle" >
175+ { solution . lastSubmission . evaluation ? (
176+ < strong className = "text-success" >
177+ < Points
178+ points = { solution . actualPoints }
179+ bonusPoints = { solution . bonusPoints }
180+ maxPoints = { solution . maxPoints }
181+ />
182+ </ strong >
183+ ) : (
184+ < span className = "text-danger" > -</ span >
185+ ) }
186+ </ td >
187+
188+ < td className = "text-center text-nowrap valign-middle" >
189+ < EnvironmentsListItem
190+ runtimeEnvironment = { runtimeEnvironments . find (
191+ ( { id } ) => id === solution . runtimeEnvironmentId
192+ ) }
134193 />
135- </ Button >
136- ) }
137- </ >
138- ) : (
139- < LoadingIcon />
140- ) }
141- </ div >
142- ) ) }
143- </ div >
144- ) ) }
145- </ div >
146- ) ) }
194+ </ td >
195+
196+ < td >
197+ < OverlayTrigger
198+ placement = "bottom"
199+ overlay = {
200+ < Tooltip id = { `review-tip-${ solution . id } ` } >
201+ < FormattedMessage
202+ id = "app.pendingReviewsList.reviewOpenedAt"
203+ defaultMessage = "Review opened at"
204+ />
205+ </ Tooltip >
206+ } >
207+ < span >
208+ < ReviewIcon review = { solution . review } className = "text-danger mr-2" />
209+ < small >
210+ < DateTime unixts = { solution . review . startedAt } />
211+ </ small >
212+ </ span >
213+ </ OverlayTrigger >
214+ </ td >
215+
216+ < td className = "shrink-col text-right" >
217+ < TheButtonGroup >
218+ { closeReview && (
219+ < Button
220+ size = "xs"
221+ variant = "success"
222+ disabled = { updatingSelector ( solution . id ) }
223+ onClick = { ( ) => closeReview ( { groupId, assignmentId, solutionId : solution . id } ) } >
224+ { updatingSelector ( solution . id ) ? (
225+ < LoadingIcon gapRight />
226+ ) : (
227+ < Icon icon = "boxes-packing" gapRight />
228+ ) }
229+ < FormattedMessage
230+ id = "app.solution.actions.review.close"
231+ defaultMessage = "Close Review"
232+ />
233+ </ Button >
234+ ) }
235+
236+ { solution . permissionHints && solution . permissionHints . viewDetail && (
237+ < >
238+ < Link to = { SOLUTION_DETAIL_URI_FACTORY ( assignmentId , solution . id ) } >
239+ < Button size = "xs" variant = "secondary" >
240+ < DetailIcon gapRight />
241+ < FormattedMessage id = "generic.detail" defaultMessage = "Detail" />
242+ </ Button >
243+ </ Link >
244+ < Link to = { SOLUTION_SOURCE_CODES_URI_FACTORY ( assignmentId , solution . id ) } >
245+ < Button size = "xs" variant = "primary" >
246+ < CodeFileIcon fixedWidth gapRight />
247+ < FormattedMessage id = "generic.files" defaultMessage = "Files" />
248+ </ Button >
249+ </ Link >
250+ </ >
251+ ) }
252+ </ TheButtonGroup >
253+ </ td >
254+ </ >
255+ ) : (
256+ < td colSpan = { 7 } >
257+ < LoadingIcon />
258+ </ td >
259+ ) }
260+ </ tr >
261+ ) )
262+ ) }
263+ </ tbody >
264+ ) ) }
265+ </ Table >
266+ ) }
147267 </ >
148268 </ Box >
149269 ) ;
@@ -153,6 +273,7 @@ class PendingReviewsList extends Component {
153273PendingReviewsList . propTypes = {
154274 solutions : PropTypes . object ,
155275 solutionsState : PropTypes . string ,
276+ runtimeEnvironments : PropTypes . array . isRequired ,
156277 updatingSelector : PropTypes . func . isRequired ,
157278 closeReview : PropTypes . func ,
158279 refresh : PropTypes . func ,
0 commit comments