Skip to content

Commit

Permalink
Making the user name format more configurable in UI (a user may selec…
Browse files Browse the repository at this point in the history
…t to show last name first in listings now).
  • Loading branch information
krulis-martin committed Jul 21, 2022
1 parent 17b0d48 commit 4775236
Show file tree
Hide file tree
Showing 17 changed files with 143 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ class ShadowAssignmentPointsTable extends Component {
userId={student.id}
showEmail="icon"
link={GROUP_USER_SOLUTIONS_URI_FACTORY(groupId, student.id)}
listItem
/>
</td>
<td className="text-center text-nowrap">{points !== null ? points : <span>&mdash;</span>}</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const ExercisesListItem = ({
</td>

<td>
<UsersNameContainer userId={authorId} link />
<UsersNameContainer userId={authorId} link listItem />
</td>

<td className="small">{runtimeEnvironments && <EnvironmentsList runtimeEnvironments={runtimeEnvironments} />}</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const ReferenceSolutionsTableRow = ({
)}
</td>
<td className="text-nowrap">
<UsersNameContainer userId={authorId} isSimple />
<UsersNameContainer userId={authorId} isSimple listItem />
</td>
</tr>
</tbody>
Expand Down
1 change: 1 addition & 0 deletions src/components/Groups/ResultsTable/ResultsTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ class ResultsTable extends Component {
currentUserId={loggedUser.id}
showEmail="icon"
showExternalIdentifiers
listItem
link={
isTeacher || user.id === loggedUser.id
? GROUP_USER_SOLUTIONS_URI_FACTORY(group.id, user.id)
Expand Down
2 changes: 1 addition & 1 deletion src/components/Instances/InstancesTable/InstancesTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const InstancesTable = ({ instances, links: { INSTANCE_URI_FACTORY }, intl }) =>
<Link to={INSTANCE_URI_FACTORY(id)}>{name}</Link>
</td>
<td>
<UsersNameContainer userId={adminId} />
<UsersNameContainer userId={adminId} listItem />
</td>
<td>
<SuccessOrFailureIcon success={hasValidLicence} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const PipelineExercisesList = ({
showEmail="icon"
noAvatar={exercises.length > COLLAPSE_LIMIT}
noAutoload
listItem
/>
</td>
<td className="shrink-col text-nowrap">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const PipelinesListItem = ({
<Link to={PIPELINE_URI_FACTORY(id)}>{name}</Link>
</td>

{showAuthor && <td>{author ? <UsersNameContainer userId={author} link /> : <i>ReCodEx</i>}</td>}
{showAuthor && <td>{author ? <UsersNameContainer userId={author} link listItem /> : <i>ReCodEx</i>}</td>}

{showCreatedAt && (
<td>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Users/StudentsListItem/StudentsListItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const StudentsListItem = ({
}) => (
<tr>
<td>
<UsersNameContainer userId={id} link={groupId && GROUP_USER_SOLUTIONS_URI_FACTORY(groupId, id)} />
<UsersNameContainer userId={id} link={groupId && GROUP_USER_SOLUTIONS_URI_FACTORY(groupId, id)} listItem />
</td>
<td width={150}>
{stats && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const SupervisorsListItem = ({
)}
</td>
<td className="text-nowrap">
<UsersNameContainer userId={id} link />
<UsersNameContainer userId={id} link listItem />
</td>

{showButtons && (
Expand Down
1 change: 1 addition & 0 deletions src/components/Users/UsersListItem/UsersListItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const UsersListItem = ({ user, emailColumn = false, createdAtColumn = false, cre
showEmail={emailColumn ? null : 'full'}
showExternalIdentifiers
link
listItem
/>
</td>

Expand Down
196 changes: 113 additions & 83 deletions src/components/Users/UsersName/UsersName.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { defaultMemoize } from 'reselect';

import AvatarContainer from '../../../containers/AvatarContainer/AvatarContainer';
import { UserRoleIcon } from '../../helpers/usersRoles';
import { UserUIDataContext } from '../../../helpers/contexts';
import NotVerified from './NotVerified';
import Icon, { MailIcon, BanIcon } from '../../icons';
import withLinks from '../../../helpers/withLinks';
Expand All @@ -25,11 +26,25 @@ const userNameStyle = defaultMemoize((size, large) => ({
const resolveLink = (link, id, USER_URI_FACTORY) =>
typeof link === 'function' ? link(id) : link === true ? USER_URI_FACTORY(id) : link;

const assembleName = ({ titlesBeforeName = '', firstName, lastName, titlesAfterName = '' }, lastNameFirst = false) => {
const fullName = lastNameFirst ? [lastName, ' ', firstName] : [firstName, ' ', lastName];
if (titlesBeforeName) {
if (lastNameFirst) {
fullName.push(', ', titlesBeforeName);
} else {
fullName.unshift(titlesBeforeName, ' ');
}
}
if (titlesAfterName) {
fullName.push(', ', titlesAfterName);
}
return fullName.join('');
};

const UsersName = ({
id,
fullName,
avatarUrl,
name: { firstName },
name,
size = null,
large = false,
isVerified,
Expand All @@ -40,103 +55,117 @@ const UsersName = ({
showExternalIdentifiers = false,
showRoleIcon = false,
currentUserId,
listItem = false,
links: { USER_URI_FACTORY },
}) => {
if (size === null) {
size = large ? 45 : 20;
}

const email = privateData && privateData.email && showEmail && encodeURIComponent(privateData.email);
const externalIds = privateData && privateData.externalIds;

return (
<span className={styles.wrapper}>
{(!privateData || privateData.isAllowed) && !noAvatar && (
<span className={styles.avatar}>
<AvatarContainer avatarUrl={avatarUrl} fullName={fullName} firstName={firstName} size={size} />
</span>
)}
<span style={userNameStyle(size, large)}>
{privateData && !privateData.isAllowed && (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id={`ban-${id}`}>
<FormattedMessage
id="app.userName.userDeactivated"
defaultMessage="The user account was deactivated. The user may not sign in."
/>
</Tooltip>
}>
<BanIcon gapRight />
</OverlayTrigger>
)}
<UserUIDataContext.Consumer>
{({ lastNameFirst = true }) => {
const fullName = assembleName(name, listItem && lastNameFirst);
return (
<span className={styles.wrapper}>
{(!privateData || privateData.isAllowed) && !noAvatar && (
<span className={styles.avatar}>
<AvatarContainer avatarUrl={avatarUrl} fullName={fullName} firstName={name.firstName} size={size} />
</span>
)}
<span style={userNameStyle(size, large)}>
{privateData && !privateData.isAllowed && (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id={`ban-${id}`}>
<FormattedMessage
id="app.userName.userDeactivated"
defaultMessage="The user account was deactivated. The user may not sign in."
/>
</Tooltip>
}>
<BanIcon gapRight />
</OverlayTrigger>
)}

{link ? <Link to={resolveLink(link, id, USER_URI_FACTORY)}>{fullName}</Link> : <span>{fullName}</span>}
{link ? <Link to={resolveLink(link, id, USER_URI_FACTORY)}>{fullName}</Link> : <span>{fullName}</span>}

{showRoleIcon && privateData && (
<UserRoleIcon
role={privateData.role}
showTooltip
tooltipId={'user-role'}
gapLeft
className="text-muted half-opaque"
/>
)}
{showRoleIcon && privateData && (
<UserRoleIcon
role={privateData.role}
showTooltip
tooltipId={'user-role'}
gapLeft
className="text-muted half-opaque"
/>
)}

{showExternalIdentifiers && externalIds && Object.keys(externalIds).length > 0 && (
<OverlayTrigger
placement="right"
overlay={
<Popover id={id}>
<Popover.Title>
<FormattedMessage id="app.userName.externalIds" defaultMessage="External identifiers" />
</Popover.Title>
<Popover.Content>
<table>
<tbody>
{Object.keys(externalIds).map(service => (
<tr key={service}>
<td className="em-padding-right">{service}:</td>
<td>
<strong>
{Array.isArray(externalIds[service])
? externalIds[service].join(', ')
: externalIds[service]}
</strong>
</td>
</tr>
))}
</tbody>
</table>
</Popover.Content>
</Popover>
}>
<Icon icon={['far', 'id-card']} gapLeft className="text-muted half-opaque" />
</OverlayTrigger>
)}
{privateData && privateData.email && showEmail === 'icon' && (
<a href={`mailto:${email}`}>
<MailIcon gapLeft />
</a>
)}
{privateData && privateData.email && showEmail === 'full' && (
<small className="em-padding-left">
{'('}
<a href={`mailto:${email}`}>{privateData.email}</a>
{')'}
</small>
)}
<span className={styles.notVerified}>
{!isVerified && <NotVerified userId={id} currentUserId={currentUserId} />}
</span>
</span>
</span>
{showExternalIdentifiers && externalIds && Object.keys(externalIds).length > 0 && (
<OverlayTrigger
placement="right"
overlay={
<Popover id={id}>
<Popover.Title>
<FormattedMessage id="app.userName.externalIds" defaultMessage="External identifiers" />
</Popover.Title>
<Popover.Content>
<table>
<tbody>
{Object.keys(externalIds).map(service => (
<tr key={service}>
<td className="em-padding-right">{service}:</td>
<td>
<strong>
{Array.isArray(externalIds[service])
? externalIds[service].join(', ')
: externalIds[service]}
</strong>
</td>
</tr>
))}
</tbody>
</table>
</Popover.Content>
</Popover>
}>
<Icon icon={['far', 'id-card']} gapLeft className="text-muted half-opaque" />
</OverlayTrigger>
)}
{privateData && privateData.email && showEmail === 'icon' && (
<a href={`mailto:${email}`}>
<MailIcon gapLeft />
</a>
)}
{privateData && privateData.email && showEmail === 'full' && (
<small className="em-padding-left">
{'('}
<a href={`mailto:${email}`}>{privateData.email}</a>
{')'}
</small>
)}
<span className={styles.notVerified}>
{!isVerified && <NotVerified userId={id} currentUserId={currentUserId} />}
</span>
</span>
</span>
);
}}
</UserUIDataContext.Consumer>
);
};

UsersName.propTypes = {
id: PropTypes.string.isRequired,
fullName: PropTypes.string.isRequired,
name: PropTypes.shape({ firstName: PropTypes.string.isRequired }).isRequired,
name: PropTypes.shape({
titlesBeforeName: PropTypes.string,
firstName: PropTypes.string.isRequired,
lastName: PropTypes.string.isRequired,
titlesAfterName: PropTypes.string,
}).isRequired,
avatarUrl: PropTypes.string,
isVerified: PropTypes.bool.isRequired,
privateData: PropTypes.object,
Expand All @@ -148,6 +177,7 @@ UsersName.propTypes = {
showExternalIdentifiers: PropTypes.bool,
showRoleIcon: PropTypes.bool,
currentUserId: PropTypes.string.isRequired,
listItem: PropTypes.bool,
links: PropTypes.object,
};

Expand Down
12 changes: 12 additions & 0 deletions src/components/forms/EditUserUIDataForm/EditUserUIDataForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ const EditUserUIDataForm = ({
}
/>

<Field
name="lastNameFirst"
component={CheckboxField}
onOff
label={
<FormattedMessage
id="app.editUserUIData.lastNameFirst"
defaultMessage="In listings, show last names of users first"
/>
}
/>

<Field
name="openedSidebar"
component={CheckboxField}
Expand Down
3 changes: 3 additions & 0 deletions src/containers/UsersNameContainer/UsersNameContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class UsersNameContainer extends Component {
showEmail = null,
showExternalIdentifiers = false,
showRoleIcon = false,
listItem = false,
} = this.props;
const size = large ? 45 : 20;
return (
Expand All @@ -58,6 +59,7 @@ class UsersNameContainer extends Component {
showEmail={showEmail}
showExternalIdentifiers={showExternalIdentifiers}
showRoleIcon={showRoleIcon}
listItem={listItem}
/>
)
}
Expand All @@ -79,6 +81,7 @@ UsersNameContainer.propTypes = {
showRoleIcon: PropTypes.bool,
noAutoload: PropTypes.bool,
loadUserIfNeeded: PropTypes.func.isRequired,
listItem: PropTypes.bool,
};

export default connect(
Expand Down
1 change: 1 addition & 0 deletions src/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@
"app.editUserUIData.defaultPage.instance": "Detail instance",
"app.editUserUIData.editorFontSize": "Velikost fontu v textovém editoru:",
"app.editUserUIData.failed": "Nebylo možné uložit nastavení vzhledu.",
"app.editUserUIData.lastNameFirst": "V seznamech zobrazovat uživatelům nejprve příjmení",
"app.editUserUIData.openedSidebar": "Postraní panel je ve výchozím stavu rozbalený",
"app.editUserUIData.title": "Nastavení vzhledu",
"app.editUserUIData.useGravatar": "Používat službu Gravatar pro zobrazování avatarů ostatních uživatelů",
Expand Down
1 change: 1 addition & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@
"app.editUserUIData.defaultPage.instance": "Instance overview",
"app.editUserUIData.editorFontSize": "Code editor font size:",
"app.editUserUIData.failed": "Cannot save visual settings of the user.",
"app.editUserUIData.lastNameFirst": "In listings, show last names of users first",
"app.editUserUIData.openedSidebar": "Sidebar is unfolded by default",
"app.editUserUIData.title": "Visual Settings",
"app.editUserUIData.useGravatar": "Use Gravatar service for fetching user avatars",
Expand Down
1 change: 1 addition & 0 deletions src/pages/AssignmentStats/AssignmentStats.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const prepareTableColumnDescriptors = defaultMemoize((loggedUserId, assignmentId
showEmail="icon"
showExternalIdentifiers
link={GROUP_USER_SOLUTIONS_URI_FACTORY(groupId, user.id)}
listItem
/>
),
}),
Expand Down
Loading

0 comments on commit 4775236

Please sign in to comment.