diff --git a/frontend/CHANGELOG.md b/frontend/CHANGELOG.md index 7e99e753dd..5c4b0b8f72 100644 --- a/frontend/CHANGELOG.md +++ b/frontend/CHANGELOG.md @@ -466,4 +466,4 @@ The latest version of this document is always available in ## [2.1.0] - 2017-01-20 -- Adjust header #51 #52 \ No newline at end of file +- Adjust header #51 #52 diff --git a/frontend/package.json b/frontend/package.json index 312e551ed3..01eb96312f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -62,6 +62,7 @@ "react-dnd": "^14.0.2", "react-dnd-html5-backend": "^14.0.0", "react-dom": "^17.0.2", + "react-outside-click-handler": "^1.3.0", "react-redux": "^7.2.3", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", @@ -71,8 +72,8 @@ "redux-mock-store": "^1.5.4", "redux-thunk": "^2.3.0", "sass": "^1.32.8", - "typescript": "^4.2.3", "swr": "^0.5.5", + "typescript": "^4.2.3", "web-vitals": "^1.0.1" }, "jest": { diff --git a/frontend/src/common.styles.js b/frontend/src/common.styles.js index f3d94e7ffc..61c6f9de38 100644 --- a/frontend/src/common.styles.js +++ b/frontend/src/common.styles.js @@ -20,11 +20,13 @@ export const useCommonStyles = makeStyles(theme => ({ margin: '1rem 0', backgroundColor: theme.palette.division.main, height: '3px', + width: '100%', }, largeDivider: { margin: '2rem 0', backgroundColor: theme.palette.division.main, height: '3px', + width: '100%', }, bold: { fontWeight: 'bold', @@ -37,6 +39,12 @@ export const useCommonStyles = makeStyles(theme => ({ display: 'flex', flexDirection: 'column', }, + itemsCenter: { + alignItems: 'center', + }, + justifyCenter: { + justifyContent: 'center', + }, flexWrap: { flexWrap: 'wrap', }, diff --git a/frontend/src/component/feature/variant/__tests__/__snapshots__/update-variant-component-test.jsx.snap b/frontend/src/component/feature/variant/__tests__/__snapshots__/update-variant-component-test.jsx.snap index e7c6aa4477..adf5ecb1ab 100644 --- a/frontend/src/component/feature/variant/__tests__/__snapshots__/update-variant-component-test.jsx.snap +++ b/frontend/src/component/feature/variant/__tests__/__snapshots__/update-variant-component-test.jsx.snap @@ -473,10 +473,10 @@ exports[`renders correctly with with variants 1`] = `
Stickiness diff --git a/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap b/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap index e0bc854072..3a3c10c57e 100644 --- a/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap +++ b/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap @@ -140,10 +140,10 @@ exports[`renders correctly with one feature 1`] = `
Project @@ -175,7 +175,7 @@ exports[`renders correctly with one feature 1`] = ` >
diff --git a/frontend/src/component/menu/Header/Header.jsx b/frontend/src/component/menu/Header/Header.jsx index 6f1e79dc08..2ce39fd127 100644 --- a/frontend/src/component/menu/Header/Header.jsx +++ b/frontend/src/component/menu/Header/Header.jsx @@ -3,12 +3,19 @@ import PropTypes from 'prop-types'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import { useTheme } from '@material-ui/core/styles'; import { Route } from 'react-router-dom'; -import { AppBar, Container, Typography, IconButton } from '@material-ui/core'; +import { + AppBar, + Container, + Typography, + IconButton, + Tooltip, +} from '@material-ui/core'; import { DrawerMenu } from '../drawer'; import MenuIcon from '@material-ui/icons/Menu'; import Breadcrumb from '../breadcrumb'; -import ShowUserContainer from '../../user/show-user-container'; +import UserProfile from '../../user/UserProfile'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; +import HelpIcon from '@material-ui/icons/Help'; import { useStyles } from './styles'; @@ -31,20 +38,36 @@ const Header = ({ uiConfig, init }) => { - + + } />
- + + + + + +
({ }, userContainer: { marginLeft: 'auto', + display: 'flex', + alignItems: 'center', + }, + docsLink: { + color: '#fff', + textDecoration: 'none', + padding: '0.25rem 0.8rem', + display: 'flex', + alignItems: 'center', + }, + docsIcon: { + height: '25px', + width: '25px', }, })); diff --git a/frontend/src/component/user/UserProfile/EditProfile/EditProfile.styles.ts b/frontend/src/component/user/UserProfile/EditProfile/EditProfile.styles.ts new file mode 100644 index 0000000000..33c8cbccb6 --- /dev/null +++ b/frontend/src/component/user/UserProfile/EditProfile/EditProfile.styles.ts @@ -0,0 +1,21 @@ +import { makeStyles } from '@material-ui/core/styles'; + +export const useStyles = makeStyles(theme => ({ + container: { + width: '100%', + transform: 'translateY(-30px)', + }, + form: { + width: '100%', + '& > *': { + width: '100%', + }, + }, + button: { + width: '150px', + marginTop: '1.15rem', + [theme.breakpoints.down('sm')]: { + width: '100px', + }, + }, +})); diff --git a/frontend/src/component/user/UserProfile/EditProfile/EditProfile.tsx b/frontend/src/component/user/UserProfile/EditProfile/EditProfile.tsx new file mode 100644 index 0000000000..c3462eb2ca --- /dev/null +++ b/frontend/src/component/user/UserProfile/EditProfile/EditProfile.tsx @@ -0,0 +1,163 @@ +import { SyntheticEvent, useState } from 'react'; +import { Button, TextField, Typography } from '@material-ui/core'; +import classnames from 'classnames'; +import { useStyles } from './EditProfile.styles'; +import { useCommonStyles } from '../../../../common.styles'; +import PasswordChecker from '../../common/ResetPasswordForm/PasswordChecker/PasswordChecker'; +import PasswordMatcher from '../../common/ResetPasswordForm/PasswordMatcher/PasswordMatcher'; +import { headers } from '../../../../store/api-helper'; +import { Alert } from '@material-ui/lab'; +import ConditionallyRender from '../../../common/ConditionallyRender'; +import useLoading from '../../../../hooks/useLoading'; +import { + BAD_REQUEST, + NOT_FOUND, + OK, + UNAUTHORIZED, +} from '../../../../constants/statusCodes'; + +interface IEditProfileProps { + setEditingProfile: React.Dispatch>; + setUpdatedPassword: React.Dispatch>; +} + +const EditProfile = ({ + setEditingProfile, + setUpdatedPassword, +}: IEditProfileProps) => { + const styles = useStyles(); + const commonStyles = useCommonStyles(); + const [loading, setLoading] = useState(false); + const [validPassword, setValidPassword] = useState(false); + const [error, setError] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const ref = useLoading(loading); + + const submit = async (e: SyntheticEvent) => { + e.preventDefault(); + + if (!validPassword || password !== confirmPassword) { + setError( + 'Password is not valid, or your passwords do not match. Please provide a password with length over 10 characters, an uppercase letter, a lowercase letter, a number and a symbol.' + ); + } else { + setLoading(true); + setError(''); + try { + const res = await fetch('api/admin/user/change-password', { + headers, + body: JSON.stringify({ password, confirmPassword }), + method: 'POST', + credentials: 'include', + }); + handleResponse(res); + } catch (e) { + setError(e); + } + } + setLoading(false); + }; + + const handleResponse = (res: Response) => { + if (res.status === BAD_REQUEST) { + setError( + 'Password could not be accepted. Please make sure you are inputting a valid password.' + ); + } + + if (res.status === UNAUTHORIZED) { + setError('You are not authorized to make this request.'); + } + + if (res.status === NOT_FOUND) { + setError( + 'The resource you requested could not be found on the server.' + ); + } + + if (res.status === OK) { + setEditingProfile(false); + setUpdatedPassword(true); + } + }; + + return ( +
+ + Update password + + + {error} + + } + /> +
+ + setPassword(e.target.value)} + /> + setConfirmPassword(e.target.value)} + /> + + + + +
+ ); +}; + +export default EditProfile; diff --git a/frontend/src/component/user/UserProfile/UserProfile.jsx b/frontend/src/component/user/UserProfile/UserProfile.jsx new file mode 100644 index 0000000000..1c4b5977d6 --- /dev/null +++ b/frontend/src/component/user/UserProfile/UserProfile.jsx @@ -0,0 +1,93 @@ +import { useEffect, useState } from 'react'; +import classnames from 'classnames'; +import PropTypes from 'prop-types'; +import OutsideClickHandler from 'react-outside-click-handler'; + +import { Avatar, Button } from '@material-ui/core'; +import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; +import { useStyles } from './UserProfile.styles'; +import { useCommonStyles } from '../../../common.styles'; +import UserProfileContent from './UserProfileContent/UserProfileContent'; + +const UserProfile = ({ + profile, + location, + fetchUser, + updateSettingLocation, + logoutUser, +}) => { + const [showProfile, setShowProfile] = useState(false); + + const styles = useStyles(); + const commonStyles = useCommonStyles(); + + const [possibleLocales, setPossibleLocales] = useState([ + { value: 'en-US', image: 'en-US' }, + { value: 'en-GB', image: 'en-GB' }, + { value: 'nb-NO', image: 'nb-NO' }, + { value: 'sv-SE', image: 'sv-SE' }, + { value: 'da-DK', image: 'da-DK' }, + { value: 'en-IN', image: 'en-IN' }, + { value: 'de', image: 'de_DE' }, + { value: 'cs', image: 'cs_CZ' }, + { value: 'pt-BR', image: 'pt_BR' }, + { value: 'fr-FR', image: 'fr-FR' }, + ]); + + useEffect(() => { + fetchUser(); + + const locale = navigator.language || navigator.userLanguage; + let found = possibleLocales.find(l => l.value === locale); + if (!found) { + setPossibleLocales(prev => ({ + ...prev, + value: locale, + image: 'unknown-locale', + })); + } + /* eslint-disable-next-line*/ + }, []); + + const email = profile ? profile.email : ''; + const imageUrl = email ? profile.imageUrl : 'unknown-user.png'; + + return ( + setShowProfile(false)}> +
+ + +
+
+ ); +}; + +UserProfile.propTypes = { + profile: PropTypes.object, + location: PropTypes.object, + fetchUser: PropTypes.func.isRequired, + updateSettingLocation: PropTypes.func.isRequired, +}; + +export default UserProfile; diff --git a/frontend/src/component/user/UserProfile/UserProfile.styles.js b/frontend/src/component/user/UserProfile/UserProfile.styles.js new file mode 100644 index 0000000000..c19edc5b97 --- /dev/null +++ b/frontend/src/component/user/UserProfile/UserProfile.styles.js @@ -0,0 +1,14 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles({ + userProfileMenu: { + display: 'flex', + }, + profileContainer: { + position: 'relative', + }, + button: { + color: '#fff', + padding: '0.2rem 1rem', + }, +}); diff --git a/frontend/src/component/user/UserProfile/UserProfileContent/UserProfileContent.jsx b/frontend/src/component/user/UserProfile/UserProfileContent/UserProfileContent.jsx new file mode 100644 index 0000000000..d7b27e9e93 --- /dev/null +++ b/frontend/src/component/user/UserProfile/UserProfileContent/UserProfileContent.jsx @@ -0,0 +1,154 @@ +import { useState } from 'react'; +import ConditionallyRender from '../../../common/ConditionallyRender'; +import { + Paper, + Avatar, + Typography, + Button, + FormControl, + Select, + InputLabel, +} from '@material-ui/core'; +import classnames from 'classnames'; +import { useStyles } from './UserProfileContent.styles'; +import { useCommonStyles } from '../../../../common.styles'; +import { Alert } from '@material-ui/lab'; +import EditProfile from '../EditProfile/EditProfile'; +import legacyStyles from '../../user.module.scss'; + +const UserProfileContent = ({ + showProfile, + profile, + possibleLocales, + updateSettingLocation, + imageUrl, + location, + logoutUser, +}) => { + const commonStyles = useCommonStyles(); + const [currentLocale, setCurrentLocale] = useState(location.locale); + const [updatedPassword, setUpdatedPassword] = useState(false); + const [edititingProfile, setEditingProfile] = useState(false); + const styles = useStyles(); + + const setLocale = value => { + updateSettingLocation('locale', value); + }; + + const profileAvatarClasses = classnames(styles.avatar, { + [styles.editingAvatar]: edititingProfile, + }); + + const profileEmailClasses = classnames(styles.profileEmail, { + [styles.editingEmail]: edititingProfile, + }); + + const handleChange = e => { + const { value } = e.target; + setCurrentLocale(value); + setLocale(value); + }; + + return ( + + + + {profile?.email} + + setUpdatedPassword(false)}> + Successfully updated password. + + } + /> + + +
+
+ + + Date/Time formatting + + + + +
+
+ + + } + elseShow={ + + } + /> + + } + /> + ); +}; + +export default UserProfileContent; diff --git a/frontend/src/component/user/UserProfile/UserProfileContent/UserProfileContent.styles.js b/frontend/src/component/user/UserProfile/UserProfileContent/UserProfileContent.styles.js new file mode 100644 index 0000000000..e6b058fde4 --- /dev/null +++ b/frontend/src/component/user/UserProfile/UserProfileContent/UserProfileContent.styles.js @@ -0,0 +1,29 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + profile: { + position: 'absolute', + zIndex: '5000', + minWidth: '300px', + right: 0, + padding: '1.5rem', + [theme.breakpoints.down('sm')]: { + width: '100%', + padding: '1rem', + }, + }, + avatar: { + width: '40px', + height: '40px', + transition: 'transform 0.4s ease', + }, + editingAvatar: { + transform: 'translateX(-102px) translateY(-9px)', + }, + profileEmail: { + transition: 'transform 0.4s ease', + }, + editingEmail: { + transform: 'translateX(10px) translateY(-60px)', + }, +})); diff --git a/frontend/src/component/user/show-user-container.jsx b/frontend/src/component/user/UserProfile/index.jsx similarity index 52% rename from frontend/src/component/user/show-user-container.jsx rename to frontend/src/component/user/UserProfile/index.jsx index acb836e5ec..1ccafe16e8 100644 --- a/frontend/src/component/user/show-user-container.jsx +++ b/frontend/src/component/user/UserProfile/index.jsx @@ -1,10 +1,11 @@ import { connect } from 'react-redux'; -import ShowUserComponent from './show-user-component'; -import { fetchUser } from '../../store/user/actions'; -import { updateSettingForGroup } from '../../store/settings/actions'; +import UserProfile from './UserProfile'; +import { fetchUser, logoutUser } from '../../../store/user/actions'; +import { updateSettingForGroup } from '../../../store/settings/actions'; const mapDispatchToProps = { fetchUser, + logoutUser, updateSettingLocation: updateSettingForGroup('location'), }; @@ -13,4 +14,4 @@ const mapStateToProps = state => ({ location: state.settings ? state.settings.toJS().location : {}, }); -export default connect(mapStateToProps, mapDispatchToProps)(ShowUserComponent); +export default connect(mapStateToProps, mapDispatchToProps)(UserProfile); diff --git a/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts b/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts index e34ad96791..55e8f473db 100644 --- a/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts +++ b/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts @@ -5,8 +5,8 @@ export const useStyles = makeStyles(theme => ({ border: '1px solid #f1f1f1', borderRadius: '3px', right: '100px', - color: '#44606e', maxWidth: '350px', + color: '#44606e', }, headerContainer: { display: 'flex', padding: '0.5rem' }, divider: { diff --git a/frontend/src/component/user/show-user-component.jsx b/frontend/src/component/user/show-user-component.jsx deleted file mode 100644 index 223fafafcc..0000000000 --- a/frontend/src/component/user/show-user-component.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import styles from './user.module.scss'; -import { MenuItem, Avatar, Typography, Icon } from '@material-ui/core'; -import DropdownMenu from '../common/DropdownMenu/DropdownMenu'; - -export default class ShowUserComponent extends React.Component { - static propTypes = { - profile: PropTypes.object, - location: PropTypes.object, - fetchUser: PropTypes.func.isRequired, - updateSettingLocation: PropTypes.func.isRequired, - }; - possibleLocales = [ - { value: 'en-US', image: 'en-US' }, - { value: 'en-GB', image: 'en-GB' }, - { value: 'nb-NO', image: 'nb-NO' }, - { value: 'sv-SE', image: 'sv-SE' }, - { value: 'da-DK', image: 'da-DK' }, - { value: 'en-IN', image: 'en-IN' }, - { value: 'de', image: 'de_DE' }, - { value: 'cs', image: 'cs_CZ' }, - { value: 'pt-BR', image: 'pt_BR' }, - { value: 'fr-FR', image: 'fr-FR' }, - ]; - - componentDidMount() { - this.props.fetchUser(); - - // find default locale and add it in choices if not present - const locale = navigator.language || navigator.userLanguage; - let found = this.possibleLocales.find(l => l.value === locale); - if (!found) { - this.possibleLocales.push({ - value: locale, - image: 'unknown-locale', - }); - } - } - - getLocale() { - return ( - (this.props.location && this.props.location.locale) || - navigator.language || - navigator.userLanguage - ); - } - - setLocale(locale) { - this.props.updateSettingLocation('locale', locale.value); - } - - render() { - const email = this.props.profile ? this.props.profile.email : ''; - const locale = this.getLocale(); - let foundLocale = this.possibleLocales.find(l => l.value === locale); - const imageUrl = email - ? this.props.profile.imageUrl - : 'unknown-user.png'; - const imageLocale = foundLocale - ? `${foundLocale.image}.png` - : `unknown-locale.png`; - return ( -
- - } - renderOptions={() => - this.possibleLocales.map(i => ( - this.setLocale(i)} - > -
- {i.value} - - {i.value} - -
-
- )) - } - label="Locale" - /> - -
- ); - } -} diff --git a/frontend/src/component/user/user.module.scss b/frontend/src/component/user/user.module.scss index 9adb8f20fd..577e1d5339 100644 --- a/frontend/src/component/user/user.module.scss +++ b/frontend/src/component/user/user.module.scss @@ -14,22 +14,23 @@ border-radius: 2px; width: 30px; height: 18px; + z-index: 6000; margin: 0 10px; } .avatar { width: 30px; height: 30px; - border-radius: 5px; - margin-left: 5px; } .showUserSettings { display: flex; align-items: center; + width: 100%; } .dropdown { - color: #fff; + color: #000; + font-weight: normal; -} \ No newline at end of file +} diff --git a/frontend/src/page/admin/users/UsersList/UsersList.jsx b/frontend/src/page/admin/users/UsersList/UsersList.jsx index 21dcb8bdf9..16e4490a20 100644 --- a/frontend/src/page/admin/users/UsersList/UsersList.jsx +++ b/frontend/src/page/admin/users/UsersList/UsersList.jsx @@ -251,4 +251,4 @@ UsersList.propTypes = { location: PropTypes.object.isRequired, }; -export default UsersList; +export default UsersList; \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock index ec76ea8eac..76da60c8a1 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2406,7 +2406,7 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -airbnb-prop-types@^2.16.0: +airbnb-prop-types@^2.15.0, airbnb-prop-types@^2.16.0: version "2.16.0" resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2" integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg== @@ -3720,6 +3720,11 @@ console-browserify@^1.1.0: resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== +"consolidated-events@^1.1.1 || ^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91" + integrity sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ== + constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" @@ -4466,6 +4471,13 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +document.contains@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/document.contains/-/document.contains-1.0.2.tgz#4260abad67a6ae9e135c1be83d68da0db169d5f0" + integrity sha512-YcvYFs15mX8m3AO1QNQy3BlIpSMfNRj3Ujk2BEJxsZG+HZf7/hZ6jr7mDpXrF8q+ff95Vef5yjhiZxm8CGJr6Q== + dependencies: + define-properties "^1.1.3" + dom-accessibility-api@^0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz#b06d059cdd4a4ad9a79275f9d414a5c126241166" @@ -9852,6 +9864,17 @@ react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react- resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-outside-click-handler@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz#3831d541ac059deecd38ec5423f81e80ad60e115" + integrity sha512-Te/7zFU0oHpAnctl//pP3hEAeobfeHMyygHB8MnjP6sX5OR8KHT1G3jmLsV3U9RnIYo+Yn+peJYWu+D5tUS8qQ== + dependencies: + airbnb-prop-types "^2.15.0" + consolidated-events "^1.1.1 || ^2.0.0" + document.contains "^1.0.1" + object.values "^1.1.0" + prop-types "^15.7.2" + react-redux@^7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.3.tgz#4c084618600bb199012687da9e42123cca3f0be9"