Skip to content

Commit

Permalink
Adding icon that will allow the user hide everyone else in the result…
Browse files Browse the repository at this point in the history
…s table, so one can better read his/her own points.
  • Loading branch information
krulis-martin committed Feb 15, 2022
1 parent 164b253 commit 6eb7f84
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 127 deletions.
299 changes: 176 additions & 123 deletions src/components/Groups/ResultsTable/ResultsTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ import { createUserNameComparator } from '../../helpers/users';
import { compareAssignments, compareShadowAssignments } from '../../helpers/assignments';
import { downloadString } from '../../../redux/helpers/api/download';
import Button from '../../widgets/TheButton';
import { DownloadIcon, LoadingIcon } from '../../icons';
import Icon, { DownloadIcon, LoadingIcon } from '../../icons';
import { safeGet, EMPTY_ARRAY, EMPTY_OBJ, hasPermissions } from '../../../helpers/common';
import withLinks from '../../../helpers/withLinks';
import { storageGetItem, storageSetItem } from '../../../helpers/localStorage';

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

const localStorageShowOnlyMeKey = 'ResultsTable.showOnlyMe';

class ResultsTable extends Component {
state = {
dialogOpen: false,
dialogClosing: false,
dialogUserId: null,
dialogAssignmentId: null,
dialogShadowId: null,
showOnlyMe: false,
};

componentDidMount = () => {
this.setState({ showOnlyMe: storageGetItem(localStorageShowOnlyMeKey, false) });
};

toggleShowOnlyMe = () => {
const showOnlyMe = !this.state.showOnlyMe;
storageSetItem(localStorageShowOnlyMeKey, showOnlyMe);
this.setState({ showOnlyMe });
};

openDialogAssignment = (dialogUserId, dialogAssignmentId) => {
Expand Down Expand Up @@ -204,151 +218,188 @@ class ResultsTable extends Component {
}
};

prepareColumnDescriptors = defaultMemoize((assignments, shadowAssignments, loggedUser, locale, isTeacher) => {
const {
group,
links: {
ASSIGNMENT_STATS_URI_FACTORY,
ASSIGNMENT_DETAIL_URI_FACTORY,
SHADOW_ASSIGNMENT_DETAIL_URI_FACTORY,
GROUP_USER_SOLUTIONS_URI_FACTORY,
},
} = this.props;

const nameComparator = createUserNameComparator(locale);

/*
* User Name (First Column)
*/
const columns = [
new SortableTableColumnDescriptor('user', <FormattedMessage id="generic.nameOfPerson" defaultMessage="Name" />, {
headerSuffix: <FormattedMessage id="app.groupResultsTable.maxPointsRow" defaultMessage="Max points:" />,
headerSuffixClassName: styles.maxPointsRow,
className: 'text-left',
comparator: ({ user: u1 }, { user: u2 }) => nameComparator(u1, u2),
cellRenderer: user =>
user && (
<UsersName
{...user}
currentUserId={loggedUser.id}
showEmail="icon"
showExternalIdentifiers
link={
isTeacher || user.id === loggedUser.id ? GROUP_USER_SOLUTIONS_URI_FACTORY(group.id, user.id) : false
}
/>
),
}),
];

/*
* Assignments
*/
assignments.sort(compareAssignments).forEach(assignment =>
columns.push(
prepareColumnDescriptors = defaultMemoize(
(assignments, shadowAssignments, loggedUser, locale, isTeacher, showOnlyMe = false) => {
const {
group,
links: {
ASSIGNMENT_STATS_URI_FACTORY,
ASSIGNMENT_DETAIL_URI_FACTORY,
SHADOW_ASSIGNMENT_DETAIL_URI_FACTORY,
GROUP_USER_SOLUTIONS_URI_FACTORY,
},
} = this.props;

const nameComparator = createUserNameComparator(locale);

/*
* User Name (First Column)
*/
const columns = [
new SortableTableColumnDescriptor(
assignment.id,
(
<div className={styles.verticalText}>
<div>
<Link
to={
isTeacher
? ASSIGNMENT_STATS_URI_FACTORY(assignment.id)
: ASSIGNMENT_DETAIL_URI_FACTORY(assignment.id)
}>
<LocalizedExerciseName entity={assignment} />
</Link>
</div>
</div>
),
'user',
<FormattedMessage id="generic.nameOfPerson" defaultMessage="Name" />,
{
headerClassName: 'text-center',
className: 'text-center',
headerSuffix:
assignment.maxPointsBeforeFirstDeadline +
(assignment.maxPointsBeforeSecondDeadline ? ` / ${assignment.maxPointsBeforeSecondDeadline}` : ''),
headerSuffix: <FormattedMessage id="app.groupResultsTable.maxPointsRow" defaultMessage="Max points:" />,
headerSuffixClassName: styles.maxPointsRow,
cellRenderer: assignmentCellRendererCreator(assignments, locale),
isClickable: hasPermissions(assignment, 'viewAssignmentSolutions')
? true
: (userId, _) => userId === loggedUser.id,
onClick: hasPermissions(assignment, 'viewAssignmentSolutions')
? (userId, assignmentId) => this.openDialogAssignment(userId, assignmentId)
: (userId, assignmentId) => userId === loggedUser.id && this.openDialogAssignment(userId, assignmentId),
className: 'text-left',
comparator: showOnlyMe ? null : ({ user: u1 }, { user: u2 }) => nameComparator(u1, u2),
cellRenderer: user =>
user && (
<>
<UsersName
{...user}
currentUserId={loggedUser.id}
showEmail="icon"
showExternalIdentifiers
link={
isTeacher || user.id === loggedUser.id
? GROUP_USER_SOLUTIONS_URI_FACTORY(group.id, user.id)
: false
}
/>
{!isTeacher && user.id === loggedUser.id && hasPermissions(group, 'viewStats') && (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id="onlyShowMe">
{showOnlyMe ? (
<FormattedMessage
id="app.resultsTable.cancelOnlyShowMe"
defaultMessage="Show all students in the group"
/>
) : (
<FormattedMessage
id="app.resultsTable.onlyShowMe"
defaultMessage="Hide everyone except for myself"
/>
)}
</Tooltip>
}>
<Icon
icon={showOnlyMe ? 'users' : ['far', 'user-circle']}
largeGapLeft
className="text-success"
onClick={this.toggleShowOnlyMe}
/>
</OverlayTrigger>
)}
</>
),
}
),
];

/*
* Assignments
*/
assignments.sort(compareAssignments).forEach(assignment =>
columns.push(
new SortableTableColumnDescriptor(
assignment.id,
(
<div className={styles.verticalText}>
<div>
<Link
to={
isTeacher
? ASSIGNMENT_STATS_URI_FACTORY(assignment.id)
: ASSIGNMENT_DETAIL_URI_FACTORY(assignment.id)
}>
<LocalizedExerciseName entity={assignment} />
</Link>
</div>
</div>
),
{
headerClassName: 'text-center',
className: 'text-center',
headerSuffix:
assignment.maxPointsBeforeFirstDeadline +
(assignment.maxPointsBeforeSecondDeadline ? ` / ${assignment.maxPointsBeforeSecondDeadline}` : ''),
headerSuffixClassName: styles.maxPointsRow,
cellRenderer: assignmentCellRendererCreator(assignments, locale),
isClickable: hasPermissions(assignment, 'viewAssignmentSolutions')
? true
: (userId, _) => userId === loggedUser.id,
onClick: hasPermissions(assignment, 'viewAssignmentSolutions')
? (userId, assignmentId) => this.openDialogAssignment(userId, assignmentId)
: (userId, assignmentId) => userId === loggedUser.id && this.openDialogAssignment(userId, assignmentId),
}
)
)
)
);
);

/*
* Shadow Assignments
*/
shadowAssignments.sort(compareShadowAssignments).forEach(shadowAssignment =>
columns.push(
new SortableTableColumnDescriptor(
shadowAssignment.id,
(
<div className={styles.verticalText}>
<div>
<Link to={SHADOW_ASSIGNMENT_DETAIL_URI_FACTORY(shadowAssignment.id)}>
<LocalizedExerciseName entity={shadowAssignment} />
</Link>
</div>
</div>
),
{
className: 'text-center',
headerSuffix: shadowAssignment.maxPoints,
headerSuffixClassName: styles.maxPointsRow,
cellRenderer: shadowAssignmentCellRendererCreator(shadowAssignments, locale),
isClickable: true,
onClick: (userId, shadowId) => this.openDialogShadowAssignment(userId, shadowId),
}
)
)
);

/*
* Shadow Assignments
*/
shadowAssignments.sort(compareShadowAssignments).forEach(shadowAssignment =>
/*
* Total points and optionally buttons
*/
columns.push(
new SortableTableColumnDescriptor(
shadowAssignment.id,
(
<div className={styles.verticalText}>
<div>
<Link to={SHADOW_ASSIGNMENT_DETAIL_URI_FACTORY(shadowAssignment.id)}>
<LocalizedExerciseName entity={shadowAssignment} />
</Link>
</div>
</div>
),
'total',
<FormattedMessage id="app.resultsTable.total" defaultMessage="Total" />,
{
className: 'text-center',
headerSuffix: shadowAssignment.maxPoints,
headerSuffixClassName: styles.maxPointsRow,
cellRenderer: shadowAssignmentCellRendererCreator(shadowAssignments, locale),
isClickable: true,
onClick: (userId, shadowId) => this.openDialogShadowAssignment(userId, shadowId),
comparator: ({ total: t1, user: u1 }, { total: t2, user: u2 }) =>
(Number(t2 && t2.gained) || -1) - (Number(t1 && t1.gained) || -1) || nameComparator(u1, u2),
cellRenderer: points => <strong>{points ? `${points.gained}/${points.total}` : '-/-'}</strong>,
}
)
)
);
);

/*
* Total points and optionally buttons
*/
columns.push(
new SortableTableColumnDescriptor(
'total',
<FormattedMessage id="app.resultsTable.total" defaultMessage="Total" />,
{
className: 'text-center',
headerSuffixClassName: styles.maxPointsRow,
comparator: ({ total: t1, user: u1 }, { total: t2, user: u2 }) =>
(Number(t2 && t2.gained) || -1) - (Number(t1 && t1.gained) || -1) || nameComparator(u1, u2),
cellRenderer: points => <strong>{points ? `${points.gained}/${points.total}` : '-/-'}</strong>,
}
)
);
if (hasPermissions(group, 'update')) {
columns.push(
new SortableTableColumnDescriptor('buttons', '', {
headerSuffixClassName: styles.maxPointsRow,
className: 'text-right',
})
);
}

if (hasPermissions(group, 'update')) {
columns.push(
new SortableTableColumnDescriptor('buttons', '', {
headerSuffixClassName: styles.maxPointsRow,
className: 'text-right',
})
);
return columns;
}

return columns;
});
);

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

return users.map(user => {
const userStats = stats.find(stat => stat.userId === user.id);
const data = {
id: user.id,
selected: !showOnlyMe && user.id === loggedUser.id,
user: user,
total: userStats && userStats.points,
buttons: renderActions && hasPermissions(group, 'update') ? renderActions(user.id) : '',
Expand Down Expand Up @@ -400,10 +451,11 @@ class ResultsTable extends Component {
shadowAssignments,
loggedUser,
locale,
isAdmin || isSupervisor || isObserver
isAdmin || isSupervisor || isObserver,
this.state.showOnlyMe
)}
defaultOrder="user"
data={this.prepareData(assignments, shadowAssignments, users, stats)}
data={this.prepareData(assignments, shadowAssignments, users, stats, this.state.showOnlyMe)}
empty={
<div className="text-center text-muted">
<FormattedMessage
Expand All @@ -412,6 +464,7 @@ class ResultsTable extends Component {
/>
</div>
}
className="hover mb-0"
/>
{(isAdmin || isSupervisor || isObserver) && (
<div className="text-center">
Expand All @@ -424,7 +477,7 @@ class ResultsTable extends Component {
getCSVValues(
assignments,
shadowAssignments,
this.prepareData(assignments, shadowAssignments, users, stats),
this.prepareData(assignments, shadowAssignments, users, stats, this.state.showOnlyMe),
locale
),
'text/csv;charset=utf-8',
Expand Down
2 changes: 1 addition & 1 deletion src/components/widgets/SortableTable/SortableTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class SortableTable extends Component {

// Default row rendering fucntion (if the user does not provide custom function)
defaultRowRenderer = (row, idx, columns) => (
<tr key={row.id || idx}>
<tr key={row.id || idx} className={row.selected ? 'selected' : ''}>
{columns.map(({ id: colId, cellRenderer, style, className, onClick, isClickable }) => {
if (typeof isClickable === 'function') {
isClickable = isClickable(row.id, colId);
Expand Down
Loading

0 comments on commit 6eb7f84

Please sign in to comment.