Skip to content

Commit 4d43889

Browse files
committed
Modifying dashboard's pending reviews list so it has similar format like review requests list.
1 parent 401e7c5 commit 4d43889

File tree

5 files changed

+207
-68
lines changed

5 files changed

+207
-68
lines changed

src/components/Solutions/PendingReviewsList/PendingReviewsList.js

Lines changed: 184 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,41 @@
11
import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
3-
import { FormattedMessage } from 'react-intl';
3+
import { FormattedMessage, FormattedNumber } from 'react-intl';
44
import { Link } from 'react-router-dom';
5+
import { Table, Badge, OverlayTrigger, Tooltip } from 'react-bootstrap';
6+
import { lruMemoize } from 'reselect';
57

68
import GroupsNameContainer from '../../../containers/GroupsNameContainer';
79
import UsersNameContainer from '../../../containers/UsersNameContainer';
810
import AssignmentNameContainer from '../../../containers/AssignmentNameContainer';
911
import DateTime from '../../widgets/DateTime';
10-
import Button from '../../widgets/TheButton';
12+
import Button, { TheButtonGroup } from '../../widgets/TheButton';
1113
import 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';
1326
import { resourceStatus } from '../../../redux/helpers/resourceManager';
27+
import { objectMap } from '../../../helpers/common.js';
1428
import 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+
1634
class 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 {
153273
PendingReviewsList.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,

src/components/Solutions/ReviewRequestsList/ReviewRequestsList.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
33
import { FormattedMessage, FormattedNumber } from 'react-intl';
44
import { Link } from 'react-router-dom';
5-
import { Table } from 'react-bootstrap';
5+
import { Table, Badge } from 'react-bootstrap';
6+
import { lruMemoize } from 'reselect';
67

78
import GroupsNameContainer from '../../../containers/GroupsNameContainer';
89
import UsersNameContainer from '../../../containers/UsersNameContainer';
@@ -24,8 +25,13 @@ import Icon, {
2425
WarningIcon,
2526
} from '../../icons';
2627
import { resourceStatus } from '../../../redux/helpers/resourceManager';
28+
import { objectMap } from '../../../helpers/common.js';
2729
import withLinks from '../../../helpers/withLinks.js';
2830

31+
const getGroupsCount = lruMemoize(
32+
groups => groups && objectMap(groups, g => Object.values(g).reduce((count, solutions) => count + solutions.length, 0))
33+
);
34+
2935
class ReviewRequestsList extends Component {
3036
state = {};
3137

@@ -40,6 +46,8 @@ class ReviewRequestsList extends Component {
4046
links: { SOLUTION_DETAIL_URI_FACTORY, SOLUTION_SOURCE_CODES_URI_FACTORY, GROUP_USER_SOLUTIONS_URI_FACTORY },
4147
} = this.props;
4248

49+
const groupCounts = getGroupsCount(solutions);
50+
4351
return solutionsState === resourceStatus.FULFILLED && (!solutions || Object.keys(solutions).length === 0) ? null : (
4452
<Box
4553
title={
@@ -80,7 +88,13 @@ class ReviewRequestsList extends Component {
8088
<GroupsNameContainer groupId={groupId} fullName translations links />
8189
</td>
8290
<td className="text-right text-muted">
91+
{this.state[`group-${groupId}`] && (
92+
<Badge variant="primary" pill className="px-2 mr-3">
93+
{groupCounts?.[groupId]}
94+
</Badge>
95+
)}
8396
<Icon
97+
className="valign-middle"
8498
icon={!this.state[`group-${groupId}`] ? 'circle-chevron-down' : 'circle-chevron-left'}
8599
gapRight
86100
timid
@@ -100,6 +114,7 @@ class ReviewRequestsList extends Component {
100114
<td>
101115
<AssignmentNameContainer assignmentId={assignmentId} solutionsLink />
102116
</td>
117+
103118
{solution ? (
104119
<>
105120
<td>

src/locales/cs.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@
176176
"app.assignmentSolutions.viewModes.grouped": "Seskupit dle uživatelů",
177177
"app.assignmentSolutions.viewModes.last": "Pouze poslední řešení",
178178
"app.assignmentSolutions.viewModes.plagiarism": "Řešení podezřelá z plagiátorství",
179+
"app.assignmentSolutions.viewModes.reviewRequests": "Žádosti o revize",
179180
"app.assignmentSolutions.viewModes.reviewed": "Pouze revidovaná řešení",
180181
"app.assignmentSolutions.viewModesTitle": "Vyberte způsob zobrazení řešení",
181182
"app.assignments.deadline": "Termín odevzdání",
@@ -1328,7 +1329,7 @@
13281329
"app.passwordStrength.unknown": "...",
13291330
"app.passwordStrength.worst": "Nevyhovující",
13301331
"app.pendingReviewsList.failed": "Načítání otevřených revizí řešení selhalo. Prosíme, zkuste po chvíli občerstvit tuto komponentu.",
1331-
"app.pendingReviewsList.reviewOpenedAt": "revize otevřena",
1332+
"app.pendingReviewsList.reviewOpenedAt": "Revize otevřena",
13321333
"app.pendingReviewsList.submitted": "odevzdáno",
13331334
"app.pendingReviewsList.title": "Všechny otevřené revize",
13341335
"app.pipeline.associatedExercises": "Přidružené úlohy",
@@ -1621,7 +1622,7 @@
16211622
"app.reviewCommentForm.suppressNotification": "Neodesílat oznámení",
16221623
"app.reviewCommentForm.suppressNotificationExplanation": "Poté, co byla revize uzavřena, se s každou provedenou změnou posílá oznámení autorovi. Můžete potlačit odeslání oznámení, pokud provádíte pouze drobnou úpravu.",
16231624
"app.reviewRequestsList.failed": "Načítání seznamu žádostí o revizi selhalo. Prosíme, zkuste po chvíli občerstvit tuto komponentu.",
1624-
"app.reviewRequestsList.title": "Solutions with requested code reviews",
1625+
"app.reviewRequestsList.title": "Řešení se žádostí o revizi",
16251626
"app.reviewSolutionButtons.closeAll": "Zavřit všechny otevřené revize",
16261627
"app.reviewSolutionButtons.closePendingReviews": "Uzavřít probíhající revize",
16271628
"app.reviewSolutionButtons.deleteConfirm": "Všechny komentáře v kódu budou smazány společně s revizí. Opravdu si přejete smazat?",

0 commit comments

Comments
 (0)