Verified Commit 00bdc319 authored by Sandro Lutz's avatar Sandro Lutz
Browse files

Implement profile page

parent 8f4cc8e5
import React from 'react'
import PropTypes from 'prop-types'
import { makeStyles } from '@material-ui/core/styles'
import { Tooltip } from '@material-ui/core'
import HelpOutlineIcon from '@material-ui/icons/HelpOutline'
const useStyles = makeStyles(
{
icon: {
verticalAlign: 'middle',
width: 16,
height: 16,
marginLeft: 2,
position: 'relative',
top: '-1px',
},
},
{ name: 'helpTooltip' }
)
const HelpTooltip = ({ title, ...props }) => {
const classes = useStyles()
return (
<Tooltip title={title} {...props}>
<HelpOutlineIcon className={classes.icon} fontSize="small" />
</Tooltip>
)
}
HelpTooltip.propTypes = {
/** Content of the tooltip */
title: PropTypes.node,
}
export default HelpTooltip
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { useDispatch } from 'react-redux'
import { FormattedMessage } from 'gatsby-plugin-intl'
import { makeStyles } from '@material-ui/core/styles'
import { Button } from '@material-ui/core'
import { enrollToGroup } from '../../store/groups/actions'
const useStyles = makeStyles(
theme => ({
root: {
width: '100%',
textAlign: 'left',
padding: '0.5em',
borderBottom: `1px solid ${theme.palette.common.grey}`,
alignItems: 'center',
'&:last-child': {
borderBottom: 'none',
},
},
name: {
flexGrow: 1,
verticalAlign: 'middle',
marginRight: '.5em',
},
}),
{ name: 'groupItem' }
)
const ProfileGroupItem = ({ item, className, ...props }) => {
const [isPending, setIsPending] = useState(false)
const classes = useStyles()
const dispatch = useDispatch()
const handleEnrollClick = () => {
setIsPending(true)
dispatch(enrollToGroup(item)).catch(() => setIsPending(false))
}
return (
<div className={[classes.root, className].join(' ')} {...props}>
<span className={classes.name}>{item.name}</span>
<Button
variant="contained"
color="primary"
disabled={isPending}
onClick={handleEnrollClick}
>
<FormattedMessage id="button.enroll" />
</Button>
</div>
)
}
ProfileGroupItem.propTypes = {
/** Group object from API */
item: PropTypes.object.isRequired,
/** @ignore */
className: PropTypes.string,
}
export default ProfileGroupItem
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage } from 'gatsby-plugin-intl'
import { makeStyles } from '@material-ui/core/styles'
import SearchField from '../general/searchField'
const useStyles = makeStyles(
{
root: {
width: '100%',
},
row: {
display: 'flex',
width: '100%',
},
noItems: {
textAlign: 'center',
padding: '3em 1em',
},
},
{ name: 'profileGroupList' }
)
const ProfileGroupList = ({
title,
items,
itemSearchTest,
itemComponent: ItemComponent,
...props
}) => {
const [search, setSearch] = useState('')
const classes = useStyles()
const handleSearchChange = event => {
setSearch(event.target.value)
}
const searchRegex = new RegExp(search)
let counter = 0
return (
<div {...props}>
<SearchField
name="search"
elevation={2}
placeholder={title}
onChange={handleSearchChange}
/>
{items.map(item => {
if (!itemSearchTest(item, searchRegex)) return null
counter += 1
return (
<ItemComponent key={item._id} className={classes.row} item={item} />
)
})}
{counter === 0 && (
<div className={classes.noItems}>
<FormattedMessage id="profile.groups.noneFound" />
</div>
)}
</div>
)
}
ProfileGroupList.propTypes = {
/** Title written as a placeholder in the search field */
title: PropTypes.string.isRequired,
/** Component type for rendering an item in the list */
itemComponent: PropTypes.elementType.isRequired,
/** Item array holding groups or groupmemberships */
items: PropTypes.array.isRequired,
/**
* Function to check whether the search applies to this item.
*
* @param {any} item
* @param {RegEx} search search regex */
itemSearchTest: PropTypes.func.isRequired,
}
export default ProfileGroupList
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { useDispatch } from 'react-redux'
import { FormattedMessage } from 'gatsby-plugin-intl'
import { makeStyles } from '@material-ui/core/styles'
import { Button } from '@material-ui/core'
import { withdrawFromGroup } from '../../store/groups/actions'
const useStyles = makeStyles(
theme => ({
root: {
width: '100%',
textAlign: 'left',
padding: '0.5em',
borderBottom: `1px solid ${theme.palette.common.grey}`,
alignItems: 'center',
'& > *': {
marginRight: '.5em',
},
'& > *:last-child': {
marginRight: 0,
},
'&:last-child': {
borderBottom: 'none',
},
},
name: {
flexGrow: 1,
verticalAlign: 'middle',
},
}),
{ name: 'groupMembershipItem' }
)
const ProfileGroupMembershipItem = ({ item, className, ...props }) => {
const [confirm, setConfirm] = useState(false)
const [isPending, setIsPending] = useState(false)
const classes = useStyles()
const dispatch = useDispatch()
const handleConfirmClick = () => {
setIsPending(true)
dispatch(withdrawFromGroup(item)).catch(() => setIsPending(false))
}
let buttons = null
if (confirm) {
buttons = (
<React.Fragment>
<Button
variant="contained"
color="primary"
disabled={isPending}
onClick={handleConfirmClick}
>
<FormattedMessage id="button.confirm" />
</Button>
<Button disabled={isPending} onClick={() => setConfirm(false)}>
<FormattedMessage id="button.cancel" />
</Button>
</React.Fragment>
)
} else {
buttons = (
<Button
variant="contained"
color="primary"
disabled={isPending}
onClick={() => setConfirm(true)}
>
<FormattedMessage id="button.withdraw" />
</Button>
)
}
return (
<div className={[classes.root, className].join(' ')} {...props}>
<span className={classes.name}>{item.group.name}</span>
{item.expiry && (
<span>
<FormattedMessage
id="profile.groups.expires"
values={{ date: item.expiry }}
/>
</span>
)}
{buttons}
</div>
)
}
ProfileGroupMembershipItem.propTypes = {
/** groupmembership object from API */
item: PropTypes.object.isRequired,
/** @ignore */
className: PropTypes.string,
}
export default ProfileGroupMembershipItem
import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useIntl } from 'gatsby-plugin-intl'
import GroupList from './_groupList'
import GroupMembershipItem from './_groupMembershipItem'
import { loadMemberships } from '../../store/groups/actions'
const ProfileGroupMemberships = props => {
const { items } = useSelector(state => state.groups.memberships)
const dispatch = useDispatch()
const intl = useIntl()
useEffect(() => {
dispatch(loadMemberships())
}, [])
const itemSearchTest = (item, searchRegex) => {
return searchRegex.test(item.group.name)
}
return (
<GroupList
title={intl.formatMessage({ id: 'profile.groups.searchEnrolled' })}
items={items}
itemSearchTest={itemSearchTest}
itemComponent={GroupMembershipItem}
{...props}
/>
)
}
export default ProfileGroupMemberships
import React, { useEffect, useCallback } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useIntl } from 'gatsby-plugin-intl'
import GroupList from './_groupList'
import GroupItem from './_groupItem'
import { loadPublicGroups } from '../../store/groups/actions'
const ProfileGroupPublic = props => {
const { items: membershipItems } = useSelector(
state => state.groups.memberships
)
const { items: groups } = useSelector(state => state.groups.publicGroups)
const dispatch = useDispatch()
const intl = useIntl()
useEffect(() => {
dispatch(loadPublicGroups())
}, [])
const items = useCallback(() => {
return groups.filter(group => {
let hasMembership = false
membershipItems.forEach(membership => {
hasMembership = hasMembership || membership.group._id === group._id
})
return !hasMembership
})
}, [membershipItems, groups])
const itemSearchTest = (item, searchRegex) => {
return searchRegex.test(item.name)
}
return (
<GroupList
title={intl.formatMessage({ id: 'profile.groups.searchPublic' })}
items={items()}
itemSearchTest={itemSearchTest}
itemComponent={GroupItem}
{...props}
/>
)
}
export default ProfileGroupPublic
import React from 'react'
import PropTypes from 'prop-types'
import { useSelector, useDispatch } from 'react-redux'
import { FormattedMessage } from 'gatsby-plugin-intl'
import { makeStyles } from '@material-ui/core/styles'
import { USER_ACTION_NEWSLETTER } from '../../store/user/constants'
import { toggleNewsletterSubscription } from '../../store/user/actions'
const useStyles = makeStyles(
theme => ({
root: {
width: '100%',
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
alignItems: 'baseline',
},
description: {
fontWeight: 800,
flexGrow: 0.5,
width: 'calc(50% - 1em)',
margin: '0 .5em',
textAlign: 'right',
},
status: {
flexGrow: 0.5,
width: 'calc(50% - 1em)',
margin: '0 .5em',
textAlign: 'left',
},
action: {
cursor: 'pointer',
},
actionDisabled: {
cursor: 'default',
color: theme.palette.info.main,
},
}),
{ name: 'newsletter' }
)
const ProfileNewsletter = ({ className, ...props }) => {
const { data: user, isPending, action } = useSelector(state => state.user)
const dispatch = useDispatch()
const classes = useStyles()
const disabled = isPending && action === USER_ACTION_NEWSLETTER
return (
<div className={[classes.root, className].join(' ')} {...props}>
<div className={classes.description}>
<FormattedMessage
id="profile.newsletter.description"
values={{ name: 'amiv Announce' }}
/>
</div>
<div className={classes.status}>
<FormattedMessage
id={
user.send_newsletter
? 'profile.newsletter.subscribed'
: 'profile.newsletter.notSubscribed'
}
/>
&nbsp;
<a
className={[
classes.action,
disabled ? classes.actionDisabled : undefined,
].join(' ')}
onClick={() => {
dispatch(toggleNewsletterSubscription())
}}
>
<FormattedMessage id="change" />
</a>
</div>
</div>
)
}
ProfileNewsletter.propTypes = {
/** @ignore */
className: PropTypes.string,
}
export default ProfileNewsletter
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { useSelector, useDispatch } from 'react-redux'
import { useIntl, FormattedMessage } from 'gatsby-plugin-intl'
import { makeStyles } from '@material-ui/core/styles'
import { Collapse, TextField, Button } from '@material-ui/core'
import Alert from '@material-ui/lab/Alert'
import HelpTooltip from '../general/helpTooltip'
import { USER_ACTION_PASSWORD } from '../../store/user/constants'
import { changePassword } from '../../store/user/actions'
const useStyles = makeStyles(
theme => ({
root: {
width: '100%',
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
},
description: {
fontWeight: 800,
flexGrow: 0.5,
width: 'calc(50% - 1em)',
margin: '0 .5em',
textAlign: 'right',
},
status: {
flexGrow: 0.5,
width: 'calc(50% - 1em)',
margin: '0 .5em',
textAlign: 'left',
},
noPassword: {
fontStyle: 'italic',
},
action: {
cursor: 'pointer',
},
content: {
maxWidth: '500px',
borderRadius: '4px',
padding: '16px',
backgroundColor: theme.palette.common.grey,
marginTop: '.5em',
textAlign: 'left',
'& > *': {
width: '100%',
},
},
field: {
margin: '.5em 0',
},
buttons: {
display: 'flex',
marginTop: '.5em',
'& > *': {
flexGrow: 1,
},
},
ldapButton: {
marginRight: '1em',
},
}),
{ name: 'password' }
)
const ProfilePassword = ({ className, ...props }) => {
const [expanded, setExpanded] = useState(false)
const [currentPassword, setCurrentPassword] = useState(null)
const [newPassword, setNewPassword] = useState(null)
const [newRepeated, setNewRepeated] = useState(null)
const { data: user, isPending, action, error } = useSelector(
state => state.user
)
const dispatch = useDispatch()
const classes = useStyles()
const intl = useIntl()
const reset = () => {
setCurrentPassword(null)
setNewPassword(null)
setNewRepeated(null)
}
const handleRevertToLdapClick = () => {
dispatch(changePassword(currentPassword, null))
reset()
}
const handleUpdateClick = () => {
dispatch(changePassword(currentPassword, newPassword))
reset()
}
const isCurrentPasswordValid = currentPassword && currentPassword.length > 0
const isNewPasswordValid =
newPassword && newPassword.length >= 7 && newPassword.length <= 100
const isNewRepeatedValid = newRepeated === newPassword
const hasPatchError = !isPending && action === USER_ACTION_PASSWORD && error
const canRevertToLdap = user.password_set
const hasPasswordSet = user.password_set
const disableRevertToLdap = !isCurrentPasswordValid
const disablePasswordChange =
!isCurrentPasswordValid || !isNewPasswordValid || !isNewRepeatedValid
let notification = null
if (hasPatchError) {
const wrongPassword =
error && error.response && error.response.status === 401
notification = (
<Alert variant="filled" severity="error">
<FormattedMessage
id={
wrongPassword
? 'profile.password.errors.current'
: 'profile.password.errors.unknown'
}
/>
</Alert>
)
} else {
notification = (
<Alert variant="filled" severity="info">
<FormattedMessage id="profile.password.explanation" />
</Alert>
)
}
return (
<div className={[classes.root, className].join(' ')} {...props}>
<div className={classes.description}>
<FormattedMessage id="profile.password" />