diff --git a/cypress/fixtures/members.js b/cypress/fixtures/members.js index ea7c44085..f597f5304 100644 --- a/cypress/fixtures/members.js +++ b/cypress/fixtures/members.js @@ -1,6 +1,20 @@ export const MEMBERS = { - ANNA: { id: 'anna-id', name: 'anna', email: 'anna@email.com' }, - BOB: { id: 'bob-id', name: 'bob', email: 'bob@email.com' }, + ANNA: { + id: 'ecafbd2a-5642-31fb-ae93-0242ac130002', + name: 'anna', + email: 'anna@email.com', + createdAt: '2021-04-13 14:56:34.749946', + extra: { + lang: 'fr', + }, + }, + BOB: { + id: 'ecafbd2a-5688-11eb-fb52-3212ac130002', + name: 'bob', + email: 'bob@email.com', + createdAt: '2021-04-13 14:56:34.749946', + extra: { lang: 'en' }, + }, }; export const CURRENT_USER = MEMBERS.ANNA; diff --git a/cypress/integration/authentication.spec.js b/cypress/integration/authentication.spec.js index 6c4c16ebd..daee7db8f 100644 --- a/cypress/integration/authentication.spec.js +++ b/cypress/integration/authentication.spec.js @@ -9,6 +9,7 @@ import { HEADER_APP_BAR_ID, HEADER_USER_ID, ITEM_MAIN_CLASS, + REDIRECTION_CONTENT_ID, USER_MENU_SIGN_OUT_OPTION_ID, } from '../../src/config/selectors'; import { SAMPLE_ITEMS } from '../fixtures/items'; @@ -16,7 +17,6 @@ import { CURRENT_USER } from '../fixtures/members'; import { REQUEST_FAILURE_LOADING_TIME, PAGE_LOAD_WAITING_PAUSE, - REDIRECTION_CONTENT, REDIRECTION_TIME, } from '../support/constants'; import { REDIRECT_URL_LOCAL_STORAGE_KEY } from '../../src/config/constants'; @@ -33,7 +33,7 @@ describe('Authentication', () => { 'equal', HOME_PATH, ); - cy.get('html').should('contain', REDIRECTION_CONTENT); + cy.get(`#${REDIRECTION_CONTENT_ID}`).should('exist'); }); it('Shared Items', () => { cy.visit(SHARED_ITEMS_PATH); @@ -42,7 +42,7 @@ describe('Authentication', () => { 'equal', SHARED_ITEMS_PATH, ); - cy.get('html').should('contain', REDIRECTION_CONTENT); + cy.get(`#${REDIRECTION_CONTENT_ID}`).should('exist'); }); }); @@ -53,11 +53,13 @@ describe('Authentication', () => { it('Signing Off redirect to sign in route', () => { cy.visit(HOME_PATH); - // user name in header cy.get(`#${HEADER_USER_ID}`).click(); cy.get(`#${USER_MENU_SIGN_OUT_OPTION_ID}`).click(); cy.wait(REQUEST_FAILURE_LOADING_TIME); - cy.get('html').should('contain', REDIRECTION_CONTENT); + + // should refetch current member just after signing out + // this current member will be unauthorized and thus redirect + cy.wait(['@signOut', '@getCurrentMember']); }); describe('Load page correctly', () => { diff --git a/cypress/integration/memberProfile.spec.js b/cypress/integration/memberProfile.spec.js new file mode 100644 index 000000000..efd822782 --- /dev/null +++ b/cypress/integration/memberProfile.spec.js @@ -0,0 +1,57 @@ +import { MEMBER_PROFILE_PATH } from '../../src/config/paths'; +import { langs } from '../../src/config/i18n'; +import { + MEMBER_PROFILE_MEMBER_ID_ID, + MEMBER_PROFILE_MEMBER_NAME_ID, + MEMBER_PROFILE_EMAIL_ID, + MEMBER_PROFILE_LANGUAGE_SWITCH_ID, + MEMBER_PROFILE_INSCRIPTION_DATE_ID, + MEMBER_PROFILE_MEMBER_ID_COPY_BUTTON_ID, +} from '../../src/config/selectors'; +import { CURRENT_USER } from '../fixtures/members'; +import { formatDate } from '../../src/utils/date'; + +describe('Member Profile', () => { + beforeEach(() => { + cy.setUpApi(); + cy.visit(MEMBER_PROFILE_PATH); + }); + + it('Layout', () => { + const { id, name, email, extra, createdAt } = CURRENT_USER; + cy.get(`#${MEMBER_PROFILE_MEMBER_ID_ID}`).should('contain', id); + cy.get(`#${MEMBER_PROFILE_MEMBER_NAME_ID}`).should('contain', name); + cy.get(`#${MEMBER_PROFILE_EMAIL_ID}`).should('contain', email); + cy.get(`#${MEMBER_PROFILE_INSCRIPTION_DATE_ID}`).should( + 'contain', + formatDate(createdAt), + ); + cy.get(`#${MEMBER_PROFILE_LANGUAGE_SWITCH_ID}`).should( + 'contain', + langs[extra.lang], + ); + }); + + it('Changing Language edits user', () => { + const { id } = CURRENT_USER; + + cy.get(`#${MEMBER_PROFILE_LANGUAGE_SWITCH_ID}`).select('en'); + + cy.wait('@editMember').then(({ request: { body, url } }) => { + expect(url).to.contain(id); + expect(body?.extra?.lang).to.equal('en'); + }); + }); + + it('Copy member ID to clipboard', () => { + const { id } = CURRENT_USER; + + cy.get(`#${MEMBER_PROFILE_MEMBER_ID_COPY_BUTTON_ID}`).click(); + + cy.window().then((win) => { + win.navigator.clipboard.readText().then((text) => { + expect(text).to.equal(id); + }); + }); + }); +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index 1da80a19a..9bfcfee54 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -27,6 +27,7 @@ module.exports = (on, config) => { root: '#root', }, API_HOST: process.env.REACT_APP_API_HOST, + AUTHENTICATION_HOST: process.env.REACT_APP_AUTHENTICATION_HOST, S3_FILES_HOST: // calls to this host are mocked, but still should be reachable // set an s3 host or fake it by using the same host as the api's diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 265101745..6aab84b3d 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -33,6 +33,8 @@ import { mockGetTags, mockPostItemTag, mockPutItemLogin, + mockEditMember, + mockGetSharedItems, } from './server'; import './commands/item'; import './commands/navigation'; @@ -62,12 +64,15 @@ Cypress.Commands.add( postItemTagError = false, postItemLoginError = false, putItemLoginError = false, + editMemberError = false, } = {}) => { const cachedItems = JSON.parse(JSON.stringify(items)); const cachedMembers = JSON.parse(JSON.stringify(members)); mockGetOwnItems(cachedItems); + mockGetSharedItems({ items: cachedItems, member: currentMember }); + mockPostItem(cachedItems, postItemError); mockDeleteItem(cachedItems, deleteItemError); @@ -118,6 +123,8 @@ Cypress.Commands.add( mockGetItemTags(items); mockPostItemTag(items, postItemTagError); + + mockEditMember(members, editMemberError); }, ); diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 0bdf33424..8b5998b4a 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -3,7 +3,7 @@ export const EDIT_ITEM_PAUSE = 1000; export const ITEM_LOGIN_PAUSE = 1000; export const NAVIGATE_PAUSE = 500; export const PAGE_LOAD_WAITING_PAUSE = 3000; -export const REQUEST_FAILURE_LOADING_TIME = 3000; +export const REQUEST_FAILURE_LOADING_TIME = 1500; export const TREE_VIEW_PAUSE = 2000; export const REDIRECTION_CONTENT = 'hello'; diff --git a/cypress/support/server.js b/cypress/support/server.js index 6de8c21fe..b31f21dcf 100644 --- a/cypress/support/server.js +++ b/cypress/support/server.js @@ -23,7 +23,6 @@ import { getItemLoginSchema, buildItemLoginSchemaExtra, } from '../../src/utils/itemExtra'; -import { REDIRECTION_CONTENT } from './constants'; import { SETTINGS } from '../../src/config/constants'; import { ITEM_LOGIN_TAG } from '../fixtures/itemTags'; @@ -53,15 +52,18 @@ const { GET_TAGS_ROUTE, buildPutItemLoginSchema, buildPostItemTagRoute, + buildPatchMember, + SHARE_ITEM_WITH_ROUTE, } = API_ROUTES; const API_HOST = Cypress.env('API_HOST'); const S3_FILES_HOST = Cypress.env('S3_FILES_HOST'); +const AUTHENTICATION_HOST = Cypress.env('AUTHENTICATION_HOST'); export const redirectionReply = { - headers: { 'content-type': 'text/html' }, + headers: { 'content-type': 'application/json' }, statusCode: StatusCodes.OK, - body: REDIRECTION_CONTENT, + body: null, }; export const mockGetCurrentMember = ( @@ -97,6 +99,19 @@ export const mockGetOwnItems = (items) => { ).as('getOwnItems'); }; +export const mockGetSharedItems = ({ items, member }) => { + cy.intercept( + { + method: DEFAULT_GET.method, + url: `${API_HOST}/${SHARE_ITEM_WITH_ROUTE}`, + }, + (req) => { + const own = items.filter(({ creator }) => creator !== member.id); + req.reply(own); + }, + ).as('getSharedItems'); +}; + export const mockPostItem = (items, shouldThrowError) => { cy.intercept( { @@ -336,6 +351,22 @@ export const mockGetMemberBy = (members, shouldThrowError) => { ).as('getMemberBy'); }; +export const mockEditMember = (members, shouldThrowError) => { + cy.intercept( + { + method: DEFAULT_PATCH.method, + url: new RegExp(`${API_HOST}/${buildPatchMember(ID_FORMAT)}`), + }, + ({ reply }) => { + if (shouldThrowError) { + return reply({ statusCode: StatusCodes.BAD_REQUEST }); + } + + return reply('edit member'); + }, + ).as('editMember'); +}; + // mock upload item for default and s3 upload methods export const mockUploadItem = (items, shouldThrowError) => { cy.intercept( @@ -425,7 +456,7 @@ export const mockSignInRedirection = () => { cy.intercept( { method: DEFAULT_GET.method, - url: new RegExp(buildSignInPath()), + url: `${AUTHENTICATION_HOST}/${buildSignInPath()}`, }, ({ reply }) => { reply(redirectionReply); diff --git a/package.json b/package.json index 9ef0112d4..205e0c4ee 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@graasp/query-client": "git://github.com/graasp/graasp-query-client.git", - "@graasp/ui": "git://github.com/graasp/graasp-ui.git", + "@graasp/ui": "git://github.com/graasp/graasp-ui.git#master", "@material-ui/core": "4.11.2", "@material-ui/icons": "4.11.2", "@material-ui/lab": "4.0.0-alpha.57", diff --git a/src/components/App.js b/src/components/App.js index 2d45fa4ed..72276c7f1 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -6,40 +6,44 @@ import { SHARED_ITEMS_PATH, buildItemPath, REDIRECT_PATH, + MEMBER_PROFILE_PATH, } from '../config/paths'; import Home from './main/Home'; import ItemScreen from './main/ItemScreen'; import SharedItems from './SharedItems'; -import Main from './main/Main'; import Authorization from './common/Authorization'; import ModalProviders from './context/ModalProviders'; import ItemLoginAuthorization from './common/ItemLoginAuthorization'; import Redirect from './main/Redirect'; +import MemberProfileScreen from './member/MemberProfileScreen'; const App = () => ( -
- - - - - - - - -
+ + + + + + + + +
); diff --git a/src/components/SharedItems.js b/src/components/SharedItems.js index ce964b3c6..980d0ca0e 100644 --- a/src/components/SharedItems.js +++ b/src/components/SharedItems.js @@ -10,6 +10,7 @@ import ErrorAlert from './common/ErrorAlert'; import Items from './main/Items'; import { hooks } from '../config/queryClient'; import Loader from './common/Loader'; +import Main from './main/Main'; const SharedItems = () => { const { t } = useTranslation(); @@ -24,14 +25,14 @@ const SharedItems = () => { } return ( - <> +
- +
); }; diff --git a/src/components/common/Authorization.js b/src/components/common/Authorization.js index 895023bcc..106ad1841 100644 --- a/src/components/common/Authorization.js +++ b/src/components/common/Authorization.js @@ -4,9 +4,11 @@ import { API_ROUTES } from '@graasp/query-client'; import { AUTHENTICATION_HOST, REDIRECT_URL_LOCAL_STORAGE_KEY, + NODE_ENV, } from '../../config/constants'; import { hooks } from '../../config/queryClient'; import Loader from './Loader'; +import RedirectPage from './RedirectionContent'; const Authorization = () => (ChildComponent) => { const ComposedComponent = (props) => { @@ -29,14 +31,26 @@ const Authorization = () => (ChildComponent) => { } // check authorization - if (isError || !currentMember) { - // save current url for later redirection after sign in - localStorage.setItem(REDIRECT_URL_LOCAL_STORAGE_KEY, pathname); - redirectToSignIn(); + if (currentMember && !isError) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; } - // eslint-disable-next-line react/jsx-props-no-spreading - return ; + // save current url for later redirection after sign in + localStorage.setItem(REDIRECT_URL_LOCAL_STORAGE_KEY, pathname); + + // do not redirect in test environment to fully load a page + // eslint-disable-next-line no-unused-expressions + NODE_ENV !== 'test' && redirectToSignIn(); + + // redirect page if redirection is not working + return ( + + ); }; return ComposedComponent; }; diff --git a/src/components/common/RedirectionContent.js b/src/components/common/RedirectionContent.js new file mode 100644 index 000000000..f16978fde --- /dev/null +++ b/src/components/common/RedirectionContent.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { Typography } from '@material-ui/core'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { REDIRECTION_CONTENT_ID } from '../../config/selectors'; + +const RedirectionContent = ({ link }) => { + const { t } = useTranslation(); + return ( + + + {t('Click here if you are not automatically redirected')} + + + ); +}; + +RedirectionContent.propTypes = { + link: PropTypes.string.isRequired, +}; + +export default RedirectionContent; diff --git a/src/components/common/SettingsHeader.js b/src/components/common/SettingsHeader.js index 589571252..46d7a9c1c 100644 --- a/src/components/common/SettingsHeader.js +++ b/src/components/common/SettingsHeader.js @@ -5,6 +5,7 @@ import Avatar from '@material-ui/core/Avatar'; import Menu from '@material-ui/core/Menu'; import Tooltip from '@material-ui/core/Tooltip'; import Box from '@material-ui/core/Box'; +import { useHistory } from 'react-router'; import { useTranslation } from 'react-i18next'; import truncate from 'lodash.truncate'; import { MUTATION_KEYS, API_ROUTES } from '@graasp/query-client'; @@ -19,6 +20,7 @@ import { USER_MENU_SIGN_OUT_OPTION_ID, } from '../../config/selectors'; import Loader from './Loader'; +import { MEMBER_PROFILE_PATH } from '../../config/paths'; const useStyles = makeStyles((theme) => ({ wrapper: { @@ -34,9 +36,10 @@ const useStyles = makeStyles((theme) => ({ }, })); -function SettingsHeader() { +const SettingsHeader = () => { const { data: user, isLoading } = hooks.useCurrentMember(); const classes = useStyles(); + const { push } = useHistory(); const { t } = useTranslation(); const [anchorEl, setAnchorEl] = useState(null); const { mutate: signOut } = useMutation(MUTATION_KEYS.SIGN_OUT); @@ -54,6 +57,10 @@ function SettingsHeader() { handleClose(); }; + const goToProfile = () => { + push(MEMBER_PROFILE_PATH); + }; + const renderMenu = () => { if (!user || user.isEmpty()) { return ( @@ -67,9 +74,12 @@ function SettingsHeader() { } return ( - - {t('Sign Out')} - + <> + {t('Profile')} + + {t('Sign Out')} + + ); }; @@ -107,6 +117,6 @@ function SettingsHeader() { ); -} +}; export default SettingsHeader; diff --git a/src/components/item/form/FolderForm.js b/src/components/item/form/FolderForm.js index 64499d973..848958b5a 100644 --- a/src/components/item/form/FolderForm.js +++ b/src/components/item/form/FolderForm.js @@ -39,11 +39,6 @@ FolderForm.propTypes = { }), }), onChange: PropTypes.func.isRequired, - classes: PropTypes.shape({ - shortInputField: PropTypes.string.isRequired, - dialogContent: PropTypes.string.isRequired, - addedMargin: PropTypes.string.isRequired, - }).isRequired, item: PropTypes.shape({ name: PropTypes.string, description: PropTypes.string, diff --git a/src/components/main/Home.js b/src/components/main/Home.js index a718957f7..8f1a6e9ec 100644 --- a/src/components/main/Home.js +++ b/src/components/main/Home.js @@ -10,6 +10,7 @@ import FileUploader from './FileUploader'; import { HOME_ERROR_ALERT_ID, OWNED_ITEMS_ID } from '../../config/selectors'; import Loader from '../common/Loader'; import ErrorAlert from '../common/ErrorAlert'; +import Main from './Main'; const Home = () => { const { t } = useTranslation(); @@ -25,11 +26,11 @@ const Home = () => { } return ( - <> +
- +
); }; diff --git a/src/components/main/ItemScreen.js b/src/components/main/ItemScreen.js index 253c2cf31..dde5b3041 100644 --- a/src/components/main/ItemScreen.js +++ b/src/components/main/ItemScreen.js @@ -25,6 +25,7 @@ import Loader from '../common/Loader'; import ErrorAlert from '../common/ErrorAlert'; import { API_HOST } from '../../config/constants'; import { ItemLayoutModeContext } from '../context/ItemLayoutModeContext'; +import Main from './Main'; const { useChildren, useItem, useFileContent, useS3FileContent } = hooks; @@ -144,7 +145,11 @@ const ItemScreen = () => { return ; } - return {renderContent()}; + return ( +
+ {renderContent()} +
+ ); }; export default ItemScreen; diff --git a/src/components/main/Main.js b/src/components/main/Main.js index 3cac10e03..c48b02dc3 100644 --- a/src/components/main/Main.js +++ b/src/components/main/Main.js @@ -1,12 +1,19 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import Drawer from '@material-ui/core/Drawer'; import clsx from 'clsx'; +import { Loader } from '@graasp/ui'; import { makeStyles } from '@material-ui/core/styles'; +import { useTranslation } from 'react-i18next'; import { CssBaseline } from '@material-ui/core'; import PropTypes from 'prop-types'; -import { HEADER_HEIGHT, LEFT_MENU_WIDTH } from '../../config/constants'; +import { + DEFAULT_LANG, + HEADER_HEIGHT, + LEFT_MENU_WIDTH, +} from '../../config/constants'; import MainMenu from './MainMenu'; import Header from '../layout/Header'; +import { hooks } from '../../config/queryClient'; const useStyles = makeStyles((theme) => ({ root: { @@ -55,9 +62,21 @@ const useStyles = makeStyles((theme) => ({ })); const Main = ({ children }) => { + const { i18n } = useTranslation(); const classes = useStyles(); const [open, setOpen] = React.useState(false); + const { data: member, isLoading } = hooks.useCurrentMember(); + + useEffect(() => { + i18n.changeLanguage(member?.get('extra')?.lang || DEFAULT_LANG); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [member?.get('extra')?.lang]); + + if (isLoading) { + return ; + } + const toggleDrawer = (isOpen) => { setOpen(isOpen); }; diff --git a/src/components/main/Redirect.js b/src/components/main/Redirect.js index 118f3b749..3b2c61e58 100644 --- a/src/components/main/Redirect.js +++ b/src/components/main/Redirect.js @@ -1,29 +1,18 @@ import React from 'react'; -import { Typography } from '@material-ui/core'; -import { Link } from 'react-router-dom'; import { useHistory } from 'react-router'; -import { useTranslation } from 'react-i18next'; import { HOME_PATH } from '../../config/paths'; import { REDIRECT_URL_LOCAL_STORAGE_KEY } from '../../config/constants'; +import RedirectionContent from '../common/RedirectionContent'; const Redirect = () => { const { push } = useHistory(); - const { t } = useTranslation(); const nextPath = localStorage.getItem(REDIRECT_URL_LOCAL_STORAGE_KEY) ?? HOME_PATH; push(nextPath); - return ( -
- - - {t('Click here if you are not automatically redirected')} - - -
- ); + return ; }; export default Redirect; diff --git a/src/components/member/LanguageSwitch.js b/src/components/member/LanguageSwitch.js new file mode 100644 index 000000000..1dfc9319d --- /dev/null +++ b/src/components/member/LanguageSwitch.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { MUTATION_KEYS } from '@graasp/query-client'; +import PropTypes from 'prop-types'; +import { FormControl, Select } from '@material-ui/core'; +import { langs } from '../../config/i18n'; +import { useMutation } from '../../config/queryClient'; + +const LanguageSwitch = ({ id, memberId, lang }) => { + const { mutate: editMember } = useMutation(MUTATION_KEYS.EDIT_MEMBER); + + const handleChange = (event) => { + editMember({ + id: memberId, + extra: { + lang: event.target.value, + }, + }); + }; + + return ( + + + + ); +}; + +LanguageSwitch.propTypes = { + id: PropTypes.string, + memberId: PropTypes.string.isRequired, + lang: PropTypes.string.isRequired, +}; + +LanguageSwitch.defaultProps = { + id: null, +}; + +export default LanguageSwitch; diff --git a/src/components/member/MemberProfileScreen.js b/src/components/member/MemberProfileScreen.js new file mode 100644 index 000000000..9a983faf5 --- /dev/null +++ b/src/components/member/MemberProfileScreen.js @@ -0,0 +1,142 @@ +import React from 'react'; +import { Grid, IconButton, makeStyles, Typography } from '@material-ui/core'; +import { Loader } from '@graasp/ui'; +import FileCopyIcon from '@material-ui/icons/FileCopy'; +import Card from '@material-ui/core/Card'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as GraaspLogo } from '../../resources/graasp-logo.svg'; +import { hooks } from '../../config/queryClient'; +import LanguageSwitch from './LanguageSwitch'; +import { formatDate } from '../../utils/date'; +import { DEFAULT_LANG } from '../../config/constants'; +import { copyToClipboard } from '../../utils/clipboard'; +import { + MEMBER_PROFILE_MEMBER_ID_ID, + MEMBER_PROFILE_EMAIL_ID, + MEMBER_PROFILE_MEMBER_NAME_ID, + MEMBER_PROFILE_INSCRIPTION_DATE_ID, + MEMBER_PROFILE_LANGUAGE_SWITCH_ID, + MEMBER_PROFILE_MEMBER_ID_COPY_BUTTON_ID, +} from '../../config/selectors'; +import notifier from '../../middlewares/notifier'; +import { COPY_MEMBER_ID_TO_CLIPBOARD } from '../../types/clipboard'; +import Main from '../main/Main'; + +const useStyles = makeStyles((theme) => ({ + profileTable: { + margin: theme.spacing(1, 0), + }, + // todo: this will be replaced by a default image or the member avatar + logo: { + background: 'grey', + textAlign: 'center', + }, +})); + +const MemberProfileScreen = () => { + const { t } = useTranslation(); + const classes = useStyles(); + const { data: member, isLoading } = hooks.useCurrentMember(); + + if (isLoading) { + return ; + } + + const copyIdToClipboard = () => { + copyToClipboard(member.get('id'), { + onSuccess: () => { + notifier({ type: COPY_MEMBER_ID_TO_CLIPBOARD.SUCCESS, payload: {} }); + }, + onError: () => { + notifier({ type: COPY_MEMBER_ID_TO_CLIPBOARD.FAILURE, payload: {} }); + }, + }); + }; + + return ( +
+ + + {/* use the member avatar */} + + + + + + + {member.get('name')} + + + {/* todo: display only as light user */} + + + {t('Member ID')} + + + + {member.get('id')} + + + + + + + + + {t('Email')} + + + + {member.get('email')} + + + + + + {t('Member Since')} + + + + {formatDate(member.get('createdAt'))} + + + + + + {t('Language')} + + + + + + + + +
+ ); +}; + +export default MemberProfileScreen; diff --git a/src/config/constants.js b/src/config/constants.js index b66ad953a..09076ebb1 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -60,6 +60,7 @@ export const MIME_TYPES = { }; export const DRAWER_WIDTH = 300; export const DEFAULT_LOCALE = 'en-US'; +export const DEFAULT_LANG = 'en'; export const DEFAULT_PERMISSION_LEVEL = PERMISSION_LEVELS.WRITE; diff --git a/src/config/messages.js b/src/config/messages.js index 9a44e97d2..7a984e016 100644 --- a/src/config/messages.js +++ b/src/config/messages.js @@ -38,3 +38,11 @@ export const POST_ITEM_TAG_ERROR_MESSAGE = 'There was an error while posting the tag.'; export const ITEM_LOGIN_SIGN_IN_ERROR_MESSAGE = 'There was an error while signing in.'; +export const EDIT_MEMBER_ERROR_MESSAGE = + 'There was an error updating the member'; +export const EDIT_MEMBER_SUCCESS_MESSAGE = + 'The member was updated successfully'; +export const COPY_MEMBER_ID_TO_CLIPBOARD_SUCCESS_MESSAGE = + 'Member ID is successfully copied!'; +export const COPY_MEMBER_ID_TO_CLIPBOARD_ERROR_MESSAGE = + 'An error occured while copying the member ID'; diff --git a/src/config/paths.js b/src/config/paths.js index e7c58ef80..e32497b4f 100644 --- a/src/config/paths.js +++ b/src/config/paths.js @@ -4,3 +4,4 @@ export const SIGN_UP_PATH = '/signUp'; export const ITEMS_PATH = '/items'; export const buildItemPath = (id = ':itemId') => `${ITEMS_PATH}/${id}`; export const REDIRECT_PATH = '/redirect'; +export const MEMBER_PROFILE_PATH = '/profile'; diff --git a/src/config/selectors.js b/src/config/selectors.js index 1ae3c893b..4b28b22c3 100644 --- a/src/config/selectors.js +++ b/src/config/selectors.js @@ -82,3 +82,12 @@ export const ITEM_FORM_APP_URL_ID = 'itemFormAppUrl'; export const VIEW_ITEM_EDIT_ITEM_BUTTON_ID = 'viewItemEditItemButton'; export const TEXT_EDITOR_CLASS = 'ql-editor'; export const buildSaveButtonId = (id) => `saveButton-${id}`; +export const MEMBER_PROFILE_MEMBER_ID_ID = 'memberProfileMemberId'; +export const MEMBER_PROFILE_MEMBER_NAME_ID = 'memberProfileMemberName'; +export const MEMBER_PROFILE_EMAIL_ID = 'memberProfileEmail'; +export const MEMBER_PROFILE_INSCRIPTION_DATE_ID = + 'memberProfileInscriptionDate'; +export const MEMBER_PROFILE_LANGUAGE_SWITCH_ID = 'memberProfileLanguageSwitch'; +export const MEMBER_PROFILE_MEMBER_ID_COPY_BUTTON_ID = + 'memberProfileMemberIdCopyButton'; +export const REDIRECTION_CONTENT_ID = 'redirectionContent'; diff --git a/src/langs/en.json b/src/langs/en.json index 1d00b4eb4..0f6395cca 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -77,6 +77,11 @@ "Create Shortcut": "Create Shortcut", "Sign Out": "Sign Out", "Confirm deleting item.": "Confirm deleting item", - "This item will be deleted permanently.": "This item will be deleted permanently." + "This item will be deleted permanently.": "This item will be deleted permanently.", + "Language": "Language", + "Storage Used": "Storage Used", + "Member Since": "Member Since", + "Profile": "Profile", + "Member ID": "Member ID" } } diff --git a/src/langs/fr.json b/src/langs/fr.json index da3bec4a9..8fcc16fb9 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -77,6 +77,11 @@ "Create Shortcut": "Créer un Raccourci", "Sign Out": "Déconnexion", "Confirm deleting item.": "Confirmer la suppression de l'élément", - "This item will be deleted permanently.": "Cet élément sera supprimé définitivement." + "This item will be deleted permanently.": "Cet élément sera supprimé définitivement.", + "Language": "Langue", + "Storage Used": "Mémoire utilisée", + "Member Since": "Membre depuis", + "Profile": "Profil", + "Member ID": "ID de Membre" } } diff --git a/src/middlewares/notifier.js b/src/middlewares/notifier.js index dc85cd5e0..8b17dbdf1 100644 --- a/src/middlewares/notifier.js +++ b/src/middlewares/notifier.js @@ -25,7 +25,12 @@ import { POST_ITEM_TAG_ERROR_MESSAGE, DELETE_ITEM_TAG_ERROR_MESSAGE, ITEM_LOGIN_SIGN_IN_ERROR_MESSAGE, + EDIT_MEMBER_ERROR_MESSAGE, + EDIT_MEMBER_SUCCESS_MESSAGE, + COPY_MEMBER_ID_TO_CLIPBOARD_SUCCESS_MESSAGE, + COPY_MEMBER_ID_TO_CLIPBOARD_ERROR_MESSAGE, } from '../config/messages'; +import { COPY_MEMBER_ID_TO_CLIPBOARD } from '../types/clipboard'; const { createItemRoutine, @@ -40,12 +45,21 @@ const { postItemTagRoutine, deleteItemTagRoutine, postItemLoginRoutine, + editMemberRoutine, } = routines; export default ({ type, payload }) => { let message = null; switch (type) { // error messages + case COPY_MEMBER_ID_TO_CLIPBOARD.FAILURE: { + message = COPY_MEMBER_ID_TO_CLIPBOARD_ERROR_MESSAGE; + break; + } + case editMemberRoutine.FAILURE: { + message = EDIT_MEMBER_ERROR_MESSAGE; + break; + } case createItemRoutine.FAILURE: { message = CREATE_ITEM_ERROR_MESSAGE; break; @@ -92,6 +106,10 @@ export default ({ type, payload }) => { break; } // success messages + case editMemberRoutine.SUCCESS: { + message = EDIT_MEMBER_SUCCESS_MESSAGE; + break; + } case createItemRoutine.SUCCESS: { message = CREATE_ITEM_SUCCESS_MESSAGE; break; @@ -125,6 +143,10 @@ export default ({ type, payload }) => { message = SIGN_OUT_SUCCESS_MESSAGE; break; } + case COPY_MEMBER_ID_TO_CLIPBOARD.SUCCESS: { + message = COPY_MEMBER_ID_TO_CLIPBOARD_SUCCESS_MESSAGE; + break; + } // progress messages // todo: this might be handled differently diff --git a/src/types/clipboard.js b/src/types/clipboard.js new file mode 100644 index 000000000..3766542c1 --- /dev/null +++ b/src/types/clipboard.js @@ -0,0 +1,6 @@ +// todo: use create routine from utils +// eslint-disable-next-line import/prefer-default-export +export const COPY_MEMBER_ID_TO_CLIPBOARD = { + SUCCESS: 'COPY_MEMBER_ID_TO_CLIPBOARD/SUCCESS', + FAILURE: 'COPY_MEMBER_ID_TO_CLIPBOARD/FAILURE', +}; diff --git a/src/utils/clipboard.js b/src/utils/clipboard.js new file mode 100644 index 000000000..900e09dcf --- /dev/null +++ b/src/utils/clipboard.js @@ -0,0 +1,15 @@ +// eslint-disable-next-line import/prefer-default-export +export const copyToClipboard = (string, { onSuccess, onError }) => { + // check can write to clipboard + navigator.permissions.query({ name: 'clipboard-write' }).then((result) => { + if (result.state === 'granted' || result.state === 'prompt') { + // write to clipboard + navigator.clipboard + .writeText(string) + .then(() => onSuccess?.()) + .catch(() => onError?.()); + } else { + onError?.(); + } + }); +}; diff --git a/yarn.lock b/yarn.lock index cac18c44d..b487bc53b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1583,9 +1583,9 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@graasp/query-client@git://github.com/graasp/graasp-query-client.git#5/patchMember": +"@graasp/query-client@git://github.com/graasp/graasp-query-client.git": version "0.1.0" - resolved "git://github.com/graasp/graasp-query-client.git#5397cd3fbb27d7703c61d57a34c7c2f1083280ff" + resolved "git://github.com/graasp/graasp-query-client.git#05bde46fec71ed84aa6ca4ae0e5753ba78c677ae" dependencies: http-status-codes "2.1.4" immutable "4.0.0-rc.12" @@ -1594,9 +1594,9 @@ react-query "3.16.0" uuid "8.3.2" -"@graasp/ui@git://github.com/graasp/graasp-ui.git#14/captions": +"@graasp/ui@git://github.com/graasp/graasp-ui.git#master": version "0.2.0" - resolved "git://github.com/graasp/graasp-ui.git#e8659334d6fe8a8e28fa4c3f41a467f58de79a69" + resolved "git://github.com/graasp/graasp-ui.git#97c65d25464dc8ba944ce35722d2bad5a16d834f" dependencies: clsx "1.1.1" immutable "4.0.0-rc.12" @@ -2313,9 +2313,9 @@ integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== "@types/node@*": - version "15.12.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.3.tgz#2817bf5f25bc82f56579018c53f7d41b1830b1af" - integrity sha512-SNt65CPCXvGNDZ3bvk1TQ0Qxoe3y1RKH88+wZ2Uf05dduBCqqFQ76ADP9pbT+Cpvj60SkRppMCh2Zo8tDixqjQ== + version "15.12.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.4.tgz#e1cf817d70a1e118e81922c4ff6683ce9d422e26" + integrity sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -4257,9 +4257,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001219: - version "1.0.30001238" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001238.tgz#e6a8b45455c5de601718736d0242feef0ecdda15" - integrity sha512-bZGam2MxEt7YNsa2VwshqWQMwrYs5tR5WZQRYSuFxsBQunWjBuXhN4cS9nV5FFb1Z9y+DoQcQ0COyQbv6A+CKw== + version "1.0.30001239" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001239.tgz#66e8669985bb2cb84ccb10f68c25ce6dd3e4d2b8" + integrity sha512-cyBkXJDMeI4wthy8xJ2FvDU6+0dtcZSJW3voUF8+e9f1bBeuvyZfc3PNbkOETyhbR+dGCPzn9E7MA3iwzusOhQ== capture-exit@^2.0.0: version "2.0.0" @@ -4963,7 +4963,7 @@ conventional-recommended-bump@6.0.11: meow "^8.0.0" q "^1.5.1" -convert-source-map@1.7.0, convert-source-map@^1.1.0, convert-source-map@^1.3.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: +convert-source-map@1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== @@ -4975,6 +4975,13 @@ convert-source-map@^0.3.3: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA= +convert-source-map@^1.1.0, convert-source-map@^1.3.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + convert-source-map@~1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" @@ -5008,17 +5015,17 @@ copy-descriptor@^0.1.0: integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= core-js-compat@^3.1.1, core-js-compat@^3.14.0, core-js-compat@^3.6.2: - version "3.14.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.14.0.tgz#b574dabf29184681d5b16357bd33d104df3d29a5" - integrity sha512-R4NS2eupxtiJU+VwgkF9WTpnSfZW4pogwKHd8bclWU2sp93Pr5S1uYJI84cMOubJRou7bcfL0vmwtLslWN5p3A== + version "3.15.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.15.0.tgz#e14a371123db9d1c5b41206d3f420643d238b8fa" + integrity sha512-8X6lWsG+s7IfOKzV93a7fRYfWRZobOfjw5V5rrq43Vh/W+V6qYxl7Akalsvgab4PFT/4L/pjQbdBUEM36NXKrw== dependencies: browserslist "^4.16.6" semver "7.0.0" core-js-pure@^3.14.0: - version "3.14.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.14.0.tgz#72bcfacba74a65ffce04bf94ae91d966e80ee553" - integrity sha512-YVh+LN2FgNU0odThzm61BsdkwrbrchumFq3oztnE9vTKC4KS2fvnPmcx8t6jnqAyOTCTF4ZSiuK8Qhh7SNcL4g== + version "3.15.0" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.15.0.tgz#c19349ae0be197b8bcf304acf4d91c5e29ae2091" + integrity sha512-RO+LFAso8DB6OeBX9BAcEGvyth36QtxYon1OyVsITNVtSKr/Hos0BXZwnsOJ7o+O6KHtK+O+cJIEj9NGg6VwFA== core-js@^2.4.0: version "2.6.12" @@ -5026,9 +5033,9 @@ core-js@^2.4.0: integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== core-js@^3.6.1, core-js@^3.6.5: - version "3.14.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.14.0.tgz#62322b98c71cc2018b027971a69419e2425c2a6c" - integrity sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA== + version "3.15.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.15.0.tgz#db9554ebce0b6fd90dc9b1f2465c841d2d055044" + integrity sha512-GUbtPllXMYRzIgHNZ4dTYTcUemls2cni83Q4Q/TrFONHfhcg9oEGOtaGHfb0cpzec60P96UKPvMkjX1jET8rUw== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -6409,9 +6416,9 @@ eslint-webpack-plugin@^2.1.0: schema-utils "^3.0.0" eslint@^7.11.0: - version "7.28.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.28.0.tgz#435aa17a0b82c13bb2be9d51408b617e49c1e820" - integrity sha512-UMfH0VSjP0G4p3EWirscJEQ/cHqnT/iuH6oNZOB94nBjWbMnhGEPxsZm1eyIW0C/9jLI0Fow4W5DXLjEI7mn1g== + version "7.29.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.29.0.tgz#ee2a7648f2e729485e4d0bd6383ec1deabc8b3c0" + integrity sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA== dependencies: "@babel/code-frame" "7.12.11" "@eslint/eslintrc" "^0.4.2" @@ -15365,9 +15372,9 @@ yargs-parser@^18.1.2: decamelize "^1.2.0" yargs-parser@^20.2.2, yargs-parser@^20.2.3: - version "20.2.7" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" - integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== yargs@^13.3.2: version "13.3.2"