Skip to content

Commit 9a0bf69

Browse files
committed
Displaying external attributes in group info table.
1 parent 4eca3ce commit 9a0bf69

5 files changed

Lines changed: 131 additions & 69 deletions

File tree

src/components/Groups/helpers/GroupInfoTable.js

Lines changed: 99 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3+
import ImmutablePropTypes from 'react-immutable-proptypes';
34
import { FormattedMessage, FormattedNumber } from 'react-intl';
45
import { Table } from 'react-bootstrap';
56

67
import Box from '../../widgets/Box';
78
import Callout from '../../widgets/Callout';
89
import Markdown from '../../widgets/Markdown';
9-
import { SuccessOrFailureIcon } from '../../icons';
10+
import { InstanceIcon, SuccessOrFailureIcon } from '../../icons';
1011
import { getLocalizedDescription } from '../../../helpers/localizedData.js';
12+
import ResourceRenderer from '../../helpers/ResourceRenderer/ResourceRenderer.js';
1113

1214
const getDescription = (localizedTexts, locale) => {
1315
const description = getLocalizedDescription({ localizedTexts }, locale);
@@ -25,6 +27,7 @@ const getDescription = (localizedTexts, locale) => {
2527

2628
const GroupInfoTable = ({
2729
group: { organizational, localizedTexts, public: isPublic = false, privateData },
30+
externalAttributes,
2831
isAdmin,
2932
locale,
3033
}) => (
@@ -36,63 +39,100 @@ const GroupInfoTable = ({
3639
collapsable
3740
noPadding
3841
unlimitedHeight>
39-
<Table>
40-
<tbody>
41-
{!organizational && privateData && (
42-
<tr>
43-
<th>
44-
<FormattedMessage
45-
id="app.groupDetail.hasPublicStats"
46-
defaultMessage="Students can see progress of other students"
47-
/>
48-
:
49-
</th>
50-
<td>
51-
<SuccessOrFailureIcon success={privateData.publicStats} />
52-
</td>
53-
</tr>
54-
)}
55-
{!organizational && (
56-
<tr>
57-
<th>
58-
<FormattedMessage id="app.groupDetail.isPublic" defaultMessage="Everyone can see and join this group" />
59-
:
60-
</th>
61-
<td>
62-
<SuccessOrFailureIcon success={isPublic} />
63-
</td>
64-
</tr>
65-
)}
66-
{privateData && Boolean(privateData.threshold) && !organizational && (
67-
<tr>
68-
<th>
69-
<FormattedMessage
70-
id="app.groupDetail.threshold"
71-
defaultMessage="Minimum percent of the total points count needed to complete the course"
72-
/>
73-
:
74-
</th>
75-
<td>
76-
<FormattedNumber value={privateData.threshold} style="percent" />
77-
</td>
78-
</tr>
79-
)}
80-
{privateData && Boolean(privateData.pointsLimit) && !organizational && (
81-
<tr>
82-
<th>
83-
<FormattedMessage
84-
id="app.groupDetail.pointsLimit"
85-
defaultMessage="Minimal amount of points needed to complete the course"
86-
/>
87-
:
88-
</th>
89-
<td>
90-
<FormattedNumber value={privateData.pointsLimit} />
91-
</td>
92-
</tr>
93-
)}
94-
</tbody>
95-
</Table>
42+
<>
43+
<Table>
44+
<tbody>
45+
{!organizational && privateData && (
46+
<tr>
47+
<th>
48+
<FormattedMessage
49+
id="app.groupDetail.hasPublicStats"
50+
defaultMessage="Students can see progress of other students"
51+
/>
52+
:
53+
</th>
54+
<td>
55+
<SuccessOrFailureIcon success={privateData.publicStats} />
56+
</td>
57+
</tr>
58+
)}
59+
{!organizational && (
60+
<tr>
61+
<th>
62+
<FormattedMessage
63+
id="app.groupDetail.isPublic"
64+
defaultMessage="Everyone can see and join this group"
65+
/>
66+
:
67+
</th>
68+
<td>
69+
<SuccessOrFailureIcon success={isPublic} />
70+
</td>
71+
</tr>
72+
)}
73+
{privateData && Boolean(privateData.threshold) && !organizational && (
74+
<tr>
75+
<th>
76+
<FormattedMessage
77+
id="app.groupDetail.threshold"
78+
defaultMessage="Minimum percent of the total points count needed to complete the course"
79+
/>
80+
:
81+
</th>
82+
<td>
83+
<FormattedNumber value={privateData.threshold} style="percent" />
84+
</td>
85+
</tr>
86+
)}
87+
{privateData && Boolean(privateData.pointsLimit) && !organizational && (
88+
<tr>
89+
<th>
90+
<FormattedMessage
91+
id="app.groupDetail.pointsLimit"
92+
defaultMessage="Minimal amount of points needed to complete the course"
93+
/>
94+
:
95+
</th>
96+
<td>
97+
<FormattedNumber value={privateData.pointsLimit} />
98+
</td>
99+
</tr>
100+
)}
101+
</tbody>
102+
</Table>
103+
104+
{externalAttributes && (
105+
<ResourceRenderer resource={externalAttributes}>
106+
{attributes => (
107+
<Table borderless>
108+
<thead>
109+
<tr>
110+
<th colSpan="3">
111+
<FormattedMessage id="app.groupDetail.externalAttributes" defaultMessage="External Attributes:" />
112+
</th>
113+
</tr>
114+
</thead>
115+
<tbody>
116+
{attributes.map(({ id, service, key, value }) => (
117+
<tr key={id}>
118+
<td className="shrink-col text-nowrap">
119+
<InstanceIcon gapLeft gapRight className="text-muted" />
120+
<code>{service}</code>
121+
</td>
122+
<td className="shrink-col text-nowrap">
123+
<code>{key}</code>
124+
</td>
125+
<td>
126+
<code>{value}</code>
127+
</td>
128+
</tr>
129+
))}
130+
</tbody>
131+
</Table>
132+
)}
133+
</ResourceRenderer>
134+
)}
135+
</>
96136
</Box>
97137
{isPublic && isAdmin && (
98138
<Callout variant="warning">
@@ -118,6 +158,7 @@ GroupInfoTable.propTypes = {
118158
publicStats: PropTypes.bool.isRequired,
119159
}),
120160
}),
161+
externalAttributes: ImmutablePropTypes.map,
121162
isAdmin: PropTypes.bool,
122163
locale: PropTypes.string.isRequired,
123164
};

src/locales/cs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,7 @@
10451045
"app.groupAssignments.groupExercises": "Úlohy k zadání ve skupině",
10461046
"app.groupDetail.assignments": "Zadané úlohy",
10471047
"app.groupDetail.description": "Popis skupiny",
1048+
"app.groupDetail.externalAttributes": "Externí atributy:",
10481049
"app.groupDetail.hasPublicStats": "Studenti smí vidět body ostatních studentů",
10491050
"app.groupDetail.isPublic": "Tuto skupinu vidí každý (a může se do ní přihlásit)",
10501051
"app.groupDetail.loading": "Načítání dat skupiny...",
@@ -2125,4 +2126,4 @@
21252126
"recodex-judge-shuffle-all": "Sudí neuspořádaných tokenů a řádků",
21262127
"recodex-judge-shuffle-newline": "Sudí neuspořádaných tokenů (ignorující konce řádků)",
21272128
"recodex-judge-shuffle-rows": "Sudí neuspořádaných řádků"
2128-
}
2129+
}

src/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,7 @@
10451045
"app.groupAssignments.groupExercises": "Exercises for Assignment in the Group",
10461046
"app.groupDetail.assignments": "Assignments",
10471047
"app.groupDetail.description": "Group Description",
1048+
"app.groupDetail.externalAttributes": "External Attributes:",
10481049
"app.groupDetail.hasPublicStats": "Students can see progress of other students",
10491050
"app.groupDetail.isPublic": "Everyone can see and join this group",
10501051
"app.groupDetail.loading": "Loading group data...",

src/pages/GroupInfo/GroupInfo.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,17 @@ import {
1414
addSupervisor,
1515
addObserver,
1616
removeMember,
17+
fetchGroupAttributes,
1718
} from '../../redux/modules/groups.js';
1819
import { fetchByIds, fetchUser } from '../../redux/modules/users.js';
1920
import { loggedInUserIdSelector } from '../../redux/selectors/auth.js';
2021
import { isLoggedAsSuperAdmin, loggedInUserSelector } from '../../redux/selectors/users.js';
21-
import { groupSelector, groupDataAccessorSelector, groupsSelector } from '../../redux/selectors/groups.js';
22+
import {
23+
groupSelector,
24+
groupDataAccessorSelector,
25+
groupsSelector,
26+
groupAttributesSelector,
27+
} from '../../redux/selectors/groups.js';
2228
import {
2329
primaryAdminsOfGroupSelector,
2430
supervisorsOfGroupSelector,
@@ -55,6 +61,7 @@ class GroupInfo extends Component {
5561
.then(res => res.value)
5662
.then(group =>
5763
Promise.all([
64+
hasPermissions(group, 'viewPublicDetail') ? dispatch(fetchGroupAttributes(groupId)) : Promise.resolve(),
5865
dispatch(fetchByIds(safeGet(group, ['primaryAdminsIds']) || [])),
5966
dispatch(fetchByIds(safeGet(group, ['privateData', 'supervisors']) || [])),
6067
dispatch(fetchByIds(safeGet(group, ['privateData', 'observers']) || [])),
@@ -109,6 +116,7 @@ class GroupInfo extends Component {
109116
isOrganizational,
110117
isExam,
111118
pendingMemberships,
119+
externalAttributes,
112120
addAdmin,
113121
addSupervisor,
114122
addObserver,
@@ -175,6 +183,7 @@ class GroupInfo extends Component {
175183
supervisors={supervisors}
176184
isAdmin={isAdminOrSuperadmin}
177185
groups={groups}
186+
externalAttributes={externalAttributes}
178187
locale={locale}
179188
/>
180189
)}
@@ -294,19 +303,20 @@ GroupInfo.propTypes = {
294303
isSupervisor: PropTypes.bool,
295304
isSuperAdmin: PropTypes.bool,
296305
isStudent: PropTypes.bool,
306+
hasThreshold: PropTypes.bool,
307+
threshold: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
308+
pointsLimit: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
309+
isOrganizational: PropTypes.bool,
310+
isExam: PropTypes.bool,
311+
pendingMemberships: ImmutablePropTypes.list,
312+
externalAttributes: ImmutablePropTypes.map,
297313
addSubgroup: PropTypes.func,
298314
loadAsync: PropTypes.func,
299315
refetchUsers: PropTypes.func.isRequired,
300316
addAdmin: PropTypes.func.isRequired,
301317
addSupervisor: PropTypes.func.isRequired,
302318
addObserver: PropTypes.func.isRequired,
303319
removeMember: PropTypes.func.isRequired,
304-
hasThreshold: PropTypes.bool,
305-
threshold: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
306-
pointsLimit: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
307-
isOrganizational: PropTypes.bool,
308-
isExam: PropTypes.bool,
309-
pendingMemberships: ImmutablePropTypes.list,
310320
links: PropTypes.object,
311321
intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired,
312322
};
@@ -336,6 +346,7 @@ const mapStateToProps = (state, { params: { groupId } }) => {
336346
isOrganizational: addSubgroupFormSelector(state, 'isOrganizational'),
337347
isExam: addSubgroupFormSelector(state, 'isExam'),
338348
pendingMemberships: pendingMembershipsSelector(state, groupId),
349+
externalAttributes: groupAttributesSelector(state, groupId),
339350
};
340351
};
341352

src/redux/modules/groups.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export const relocateGroup = (groupId, newParentId) =>
165165
endpoint: `/groups/${groupId}/relocate/${newParentId}`,
166166
});
167167

168-
export const getGroupAttributes = groupId =>
168+
export const fetchGroupAttributes = groupId =>
169169
createApiAction({
170170
type: additionalActionTypes.GET_ATTRIBUTES,
171171
method: 'GET',
@@ -228,6 +228,11 @@ export const unlockStudentFromExam = (groupId, userId) =>
228228
* Reducer
229229
*/
230230

231+
const sortAttributes = attributes =>
232+
attributes.sort(
233+
(a, b) => a.service.localeCompare(b.service) || a.key.localeCompare(b.key) || a.value.localeCompare(b.value)
234+
);
235+
231236
const reducer = handleActions(
232237
Object.assign({}, reduceActions, {
233238
[actionTypes.ADD_FULFILLED]: (state, action) => {
@@ -335,8 +340,11 @@ const reducer = handleActions(
335340
[additionalActionTypes.GET_ATTRIBUTES_PENDING]: (state, { meta: { groupId } }) =>
336341
state.setIn(['attributes', groupId], createRecord()),
337342

338-
[additionalActionTypes.GET_ATTRIBUTES_FULFILLED]: (state, { meta: { groupId }, payload: data }) =>
339-
state.setIn(['attributes', groupId], createRecord({ state: resourceStatus.FULFILLED, data })),
343+
[additionalActionTypes.GET_ATTRIBUTES_FULFILLED]: (state, { meta: { groupId }, payload }) =>
344+
state.setIn(
345+
['attributes', groupId],
346+
createRecord({ state: resourceStatus.FULFILLED, data: sortAttributes(payload) })
347+
),
340348

341349
[additionalActionTypes.GET_ATTRIBUTES_REJECTED]: (state, { meta: { groupId }, payload: error }) =>
342350
state.setIn(['attributes', groupId], createRecord({ state: resourceStatus.FAILED, error })),

0 commit comments

Comments
 (0)