-
+
- {this.props.srMessage}
-
- );
- }
-
- render() {
- return (
-
-
-
- {this.renderSrMessage()}
-
-
+const PageLoading = ({ srMessage }) => (
+
+
+
+ {srMessage && {srMessage} }
- );
- }
-}
+
+
+);
PageLoading.propTypes = {
srMessage: PropTypes.string.isRequired,
};
+
+export default PageLoading;
diff --git a/src/profile/ProfilePage.jsx b/src/profile/ProfilePage.jsx
index 2dfe72d..2411755 100644
--- a/src/profile/ProfilePage.jsx
+++ b/src/profile/ProfilePage.jsx
@@ -1,14 +1,20 @@
-import React from 'react';
+import React, {
+ useEffect, useState, useContext, useCallback,
+} from 'react';
import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
+import { useNavigate } from 'react-router-dom';
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
-import { ensureConfig, getConfig } from '@edx/frontend-platform';
+import { ensureConfig } from '@edx/frontend-platform';
import { AppContext } from '@edx/frontend-platform/react';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { Alert, Hyperlink } from '@openedx/paragon';
+import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
+import {
+ Alert, Hyperlink, OverlayTrigger, Tooltip,
+} from '@openedx/paragon';
+import { InfoOutline } from '@openedx/paragon/icons';
+import classNames from 'classnames';
-// Actions
import {
fetchProfile,
saveProfile,
@@ -19,7 +25,6 @@ import {
updateDraft,
} from './data/actions';
-// Components
import ProfileAvatar from './forms/ProfileAvatar';
import Name from './forms/Name';
import Country from './forms/Country';
@@ -27,120 +32,122 @@ import PreferredLanguage from './forms/PreferredLanguage';
import Education from './forms/Education';
import SocialLinks from './forms/SocialLinks';
import Bio from './forms/Bio';
-import Certificates from './forms/Certificates';
-import AgeMessage from './AgeMessage';
import DateJoined from './DateJoined';
-import UsernameDescription from './UsernameDescription';
+import UserCertificateSummary from './UserCertificateSummary';
import PageLoading from './PageLoading';
-import Banner from './Banner';
-import LearningGoal from './forms/LearningGoal';
+import Certificates from './Certificates';
-// Selectors
import { profilePageSelector } from './data/selectors';
-
-// i18n
import messages from './ProfilePage.messages';
-
import withParams from '../utils/hoc';
+import { useIsOnMobileScreen, useIsOnTabletScreen } from './data/hooks';
-ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
+ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL', 'ACCOUNT_SETTINGS_URL'], 'ProfilePage');
-class ProfilePage extends React.Component {
- constructor(props, context) {
- super(props, context);
+const ProfilePage = ({ params }) => {
+ const dispatch = useDispatch();
+ const intl = useIntl();
+ const context = useContext(AppContext);
+ const {
+ dateJoined,
+ courseCertificates,
+ name,
+ visibilityName,
+ profileImage,
+ savePhotoState,
+ isLoadingProfile,
+ photoUploadError,
+ country,
+ visibilityCountry,
+ levelOfEducation,
+ visibilityLevelOfEducation,
+ socialLinks,
+ draftSocialLinksByPlatform,
+ visibilitySocialLinks,
+ languageProficiencies,
+ visibilityLanguageProficiencies,
+ bio,
+ visibilityBio,
+ saveState,
+ username,
+ } = useSelector(profilePageSelector);
- const credentialsBaseUrl = context.config.CREDENTIALS_BASE_URL;
- this.state = {
- viewMyRecordsUrl: credentialsBaseUrl ? `${credentialsBaseUrl}/records` : null,
- accountSettingsUrl: context.config.ACCOUNT_SETTINGS_URL,
- };
+ const navigate = useNavigate();
+ const [viewMyRecordsUrl, setViewMyRecordsUrl] = useState(null);
+ const isMobileView = useIsOnMobileScreen();
+ const isTabletView = useIsOnTabletScreen();
- this.handleSaveProfilePhoto = this.handleSaveProfilePhoto.bind(this);
- this.handleDeleteProfilePhoto = this.handleDeleteProfilePhoto.bind(this);
- this.handleClose = this.handleClose.bind(this);
- this.handleOpen = this.handleOpen.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleChange = this.handleChange.bind(this);
- }
+ useEffect(() => {
+ const { CREDENTIALS_BASE_URL } = context.config;
+ if (CREDENTIALS_BASE_URL) {
+ setViewMyRecordsUrl(`${CREDENTIALS_BASE_URL}/records`);
+ }
- componentDidMount() {
- this.props.fetchProfile(this.props.params.username);
+ dispatch(fetchProfile(params.username));
sendTrackingLogEvent('edx.profile.viewed', {
- username: this.props.params.username,
+ username: params.username,
});
- }
+ }, [dispatch, params.username, context.config]);
- handleSaveProfilePhoto(formData) {
- this.props.saveProfilePhoto(this.context.authenticatedUser.username, formData);
- }
+ useEffect(() => {
+ if (!username && saveState === 'error' && navigate) {
+ navigate('/notfound');
+ }
+ }, [username, saveState, navigate]);
- handleDeleteProfilePhoto() {
- this.props.deleteProfilePhoto(this.context.authenticatedUser.username);
- }
+ const authenticatedUserName = context.authenticatedUser.username;
- handleClose(formId) {
- this.props.closeForm(formId);
- }
+ const handleSaveProfilePhoto = useCallback((formData) => {
+ dispatch(saveProfilePhoto(authenticatedUserName, formData));
+ }, [dispatch, authenticatedUserName]);
- handleOpen(formId) {
- this.props.openForm(formId);
- }
+ const handleDeleteProfilePhoto = useCallback(() => {
+ dispatch(deleteProfilePhoto(authenticatedUserName));
+ }, [dispatch, authenticatedUserName]);
- handleSubmit(formId) {
- this.props.saveProfile(formId, this.context.authenticatedUser.username);
- }
+ const handleClose = useCallback((formId) => {
+ dispatch(closeForm(formId));
+ }, [dispatch]);
- handleChange(name, value) {
- this.props.updateDraft(name, value);
- }
+ const handleOpen = useCallback((formId) => {
+ dispatch(openForm(formId));
+ }, [dispatch]);
- isYOBDisabled() {
- const { yearOfBirth } = this.props;
- const currentYear = new Date().getFullYear();
- const isAgeOrNotCompliant = !yearOfBirth || ((currentYear - yearOfBirth) < 13);
+ const handleSubmit = useCallback((formId) => {
+ dispatch(saveProfile(formId, authenticatedUserName));
+ }, [dispatch, authenticatedUserName]);
- return isAgeOrNotCompliant && getConfig().COLLECT_YEAR_OF_BIRTH !== 'true';
- }
+ const handleChange = useCallback((fieldName, value) => {
+ dispatch(updateDraft(fieldName, value));
+ }, [dispatch]);
- isAuthenticatedUserProfile() {
- return this.props.params.username === this.context.authenticatedUser.username;
- }
+ const isAuthenticatedUserProfile = () => params.username === authenticatedUserName;
- // Inserted into the DOM in two places (for responsive layout)
- renderViewMyRecordsButton() {
- if (!(this.state.viewMyRecordsUrl && this.isAuthenticatedUserProfile())) {
+ const isBlockVisible = (blockInfo) => isAuthenticatedUserProfile()
+ || (!isAuthenticatedUserProfile() && Boolean(blockInfo));
+
+ const renderViewMyRecordsButton = () => {
+ if (!(viewMyRecordsUrl && isAuthenticatedUserProfile())) {
return null;
}
return (
-
- {this.props.intl.formatMessage(messages['profile.viewMyRecords'])}
+
+ {intl.formatMessage(messages['profile.viewMyRecords'])}
);
- }
+ };
- // Inserted into the DOM in two places (for responsive layout)
- renderHeadingLockup() {
- const { dateJoined } = this.props;
-
- return (
-
- {this.props.params.username}
-
- {this.isYOBDisabled() && }
-
-
- );
- }
-
- renderPhotoUploadErrorMessage() {
- const { photoUploadError } = this.props;
-
- if (photoUploadError === null) {
- return null;
- }
-
- return (
+ const renderPhotoUploadErrorMessage = () => (
+ photoUploadError && (
@@ -148,227 +155,267 @@ class ProfilePage extends React.Component {
- );
- }
+ )
+ );
- renderAgeMessage() {
- const { requiresParentalConsent } = this.props;
- const shouldShowAgeMessage = requiresParentalConsent && this.isAuthenticatedUserProfile();
+ const commonFormProps = {
+ openHandler: handleOpen,
+ closeHandler: handleClose,
+ submitHandler: handleSubmit,
+ changeHandler: handleChange,
+ };
- if (!shouldShowAgeMessage) {
- return null;
- }
- return ;
- }
-
- renderContent() {
- const {
- profileImage,
- name,
- visibilityName,
- country,
- visibilityCountry,
- levelOfEducation,
- visibilityLevelOfEducation,
- socialLinks,
- draftSocialLinksByPlatform,
- visibilitySocialLinks,
- learningGoal,
- visibilityLearningGoal,
- languageProficiencies,
- visibilityLanguageProficiencies,
- courseCertificates,
- visibilityCourseCertificates,
- bio,
- visibilityBio,
- requiresParentalConsent,
- isLoadingProfile,
- username,
- saveState,
- navigate,
- } = this.props;
-
- if (isLoadingProfile) {
- return ;
- }
-
- if (!username && saveState === 'error' && navigate) {
- navigate('/notfound');
- }
-
- const commonFormProps = {
- openHandler: this.handleOpen,
- closeHandler: this.handleClose,
- submitHandler: this.handleSubmit,
- changeHandler: this.handleChange,
- };
-
- const isBlockVisible = (blockInfo) => this.isAuthenticatedUserProfile()
- || (!this.isAuthenticatedUserProfile() && Boolean(blockInfo));
-
- const isLanguageBlockVisible = isBlockVisible(languageProficiencies.length);
- const isEducationBlockVisible = isBlockVisible(levelOfEducation);
- const isSocialLinksBLockVisible = isBlockVisible(socialLinks.some((link) => link.socialLink !== null));
- const isBioBlockVisible = isBlockVisible(bio);
- const isCertificatesBlockVisible = isBlockVisible(courseCertificates.length);
- const isNameBlockVisible = isBlockVisible(name);
- const isLocationBlockVisible = isBlockVisible(country);
-
- return (
-
-
-
-
-
+ return (
+
+ {isLoadingProfile ? (
+
+ ) : (
+ <>
+
+
+
+
+
+
+
+ {params.username}
+
+ {isBlockVisible(name) && (
+
+ {name}
+
+ )}
+
+
+
+
+
+
+ {renderViewMyRecordsButton()}
+
+
+
+
+ {renderPhotoUploadErrorMessage()}
+
-
-
- {this.renderHeadingLockup()}
-
-
- {this.renderViewMyRecordsButton()}
+
+
+
+
+
+ {isMobileView ? (
+
+ )
+ : (
+
+ )}
+
+
+
+
+
+
+
+
+ {intl.formatMessage(messages['profile.username'])}
+
+
+
+ {intl.formatMessage(messages['profile.username.tooltip'])}
+
+
+ )}
+ >
+
+
+
+
+ {params.username}
+
+
+ {isBlockVisible(name) && (
+
+ )}
+ {isBlockVisible(country) && (
+
+ )}
+ {isBlockVisible((languageProficiencies || []).length) && (
+
+ )}
+ {isBlockVisible(levelOfEducation) && (
+
+ )}
+
+
+ {isBlockVisible(bio) && (
+
+ )}
+ {isBlockVisible((socialLinks || []).some((link) => link?.socialLink !== null)) && (
+
+ )}
+
+
-
- {this.renderPhotoUploadErrorMessage()}
-
-
-
- {this.renderHeadingLockup()}
-
-
- {this.renderViewMyRecordsButton()}
-
- {isNameBlockVisible && (
-
- )}
- {isLocationBlockVisible && (
-
- )}
- {isLanguageBlockVisible && (
-
- )}
- {isEducationBlockVisible && (
-
- )}
- {isSocialLinksBLockVisible && (
-
+
+ {isBlockVisible((courseCertificates || []).length) && (
+
)}
-
- {!this.isYOBDisabled() && this.renderAgeMessage()}
- {isBioBlockVisible && (
-
- )}
- {getConfig().ENABLE_SKILLS_BUILDER_PROFILE && (
-
- )}
- {isCertificatesBlockVisible && (
-
- )}
-
-
-
- );
- }
-
- render() {
- return (
-
-
- {this.renderContent()}
-
- );
- }
-}
-
-ProfilePage.contextType = AppContext;
+ >
+ )}
+
+ );
+};
ProfilePage.propTypes = {
- // Account data
+ params: PropTypes.shape({
+ username: PropTypes.string.isRequired,
+ }).isRequired,
requiresParentalConsent: PropTypes.bool,
dateJoined: PropTypes.string,
username: PropTypes.string,
-
- // Bio form data
bio: PropTypes.string,
- yearOfBirth: PropTypes.number,
- visibilityBio: PropTypes.string.isRequired,
-
- // Certificates form data
+ visibilityBio: PropTypes.string,
courseCertificates: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string,
})),
- visibilityCourseCertificates: PropTypes.string.isRequired,
-
- // Country form data
country: PropTypes.string,
- visibilityCountry: PropTypes.string.isRequired,
-
- // Education form data
+ visibilityCountry: PropTypes.string,
levelOfEducation: PropTypes.string,
- visibilityLevelOfEducation: PropTypes.string.isRequired,
-
- // Language proficiency form data
+ visibilityLevelOfEducation: PropTypes.string,
languageProficiencies: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
})),
- visibilityLanguageProficiencies: PropTypes.string.isRequired,
-
- // Name form data
+ visibilityLanguageProficiencies: PropTypes.string,
name: PropTypes.string,
- visibilityName: PropTypes.string.isRequired,
-
- // Social links form data
+ visibilityName: PropTypes.string,
socialLinks: PropTypes.arrayOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
@@ -377,41 +424,15 @@ ProfilePage.propTypes = {
platform: PropTypes.string,
socialLink: PropTypes.string,
})),
- visibilitySocialLinks: PropTypes.string.isRequired,
-
- // Learning Goal form data
- learningGoal: PropTypes.string,
- visibilityLearningGoal: PropTypes.string.isRequired,
-
- // Other data we need
+ visibilitySocialLinks: PropTypes.string,
profileImage: PropTypes.shape({
src: PropTypes.string,
isDefault: PropTypes.bool,
}),
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
- isLoadingProfile: PropTypes.bool.isRequired,
-
- // Page state helpers
+ isLoadingProfile: PropTypes.bool,
photoUploadError: PropTypes.objectOf(PropTypes.string),
-
- // Actions
- fetchProfile: PropTypes.func.isRequired,
- saveProfile: PropTypes.func.isRequired,
- saveProfilePhoto: PropTypes.func.isRequired,
- deleteProfilePhoto: PropTypes.func.isRequired,
- openForm: PropTypes.func.isRequired,
- closeForm: PropTypes.func.isRequired,
- updateDraft: PropTypes.func.isRequired,
- navigate: PropTypes.func.isRequired,
-
- // Router
- params: PropTypes.shape({
- username: PropTypes.string.isRequired,
- }).isRequired,
-
- // i18n
- intl: intlShape.isRequired,
};
ProfilePage.defaultProps = {
@@ -421,28 +442,22 @@ ProfilePage.defaultProps = {
photoUploadError: {},
profileImage: {},
name: null,
- yearOfBirth: null,
levelOfEducation: null,
country: null,
socialLinks: [],
draftSocialLinksByPlatform: {},
bio: null,
- learningGoal: null,
languageProficiencies: [],
- courseCertificates: null,
+ courseCertificates: [],
requiresParentalConsent: null,
dateJoined: null,
+ visibilityName: null,
+ visibilityCountry: null,
+ visibilityLevelOfEducation: null,
+ visibilitySocialLinks: null,
+ visibilityLanguageProficiencies: null,
+ visibilityBio: null,
+ isLoadingProfile: false,
};
-export default connect(
- profilePageSelector,
- {
- fetchProfile,
- saveProfilePhoto,
- deleteProfilePhoto,
- saveProfile,
- openForm,
- closeForm,
- updateDraft,
- },
-)(injectIntl(withParams(ProfilePage)));
+export default withParams(ProfilePage);
diff --git a/src/profile/ProfilePage.messages.jsx b/src/profile/ProfilePage.messages.jsx
index 4dfeef6..c7cec79 100644
--- a/src/profile/ProfilePage.messages.jsx
+++ b/src/profile/ProfilePage.messages.jsx
@@ -11,6 +11,16 @@ const messages = defineMessages({
defaultMessage: 'Profile loading...',
description: 'Message displayed when the profile data is loading.',
},
+ 'profile.username': {
+ id: 'profile.username',
+ defaultMessage: 'Username',
+ description: 'Label for the username field.',
+ },
+ 'profile.username.tooltip': {
+ id: 'profile.username.tooltip',
+ defaultMessage: 'The name that identifies you on edX. You cannot change your username.',
+ description: 'Tooltip for the username field.',
+ },
});
export default messages;
diff --git a/src/profile/ProfilePage.test.jsx b/src/profile/ProfilePage.test.jsx
index 7f1a771..d186c48 100644
--- a/src/profile/ProfilePage.test.jsx
+++ b/src/profile/ProfilePage.test.jsx
@@ -1,4 +1,3 @@
-/* eslint-disable global-require */
import { getConfig } from '@edx/frontend-platform';
import * as analytics from '@edx/frontend-platform/analytics';
import { AppContext } from '@edx/frontend-platform/react';
@@ -9,31 +8,38 @@ import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
-import { BrowserRouter, useNavigate } from 'react-router-dom';
+import {
+ MemoryRouter,
+ Routes,
+ Route,
+ useNavigate,
+} from 'react-router-dom';
import messages from '../i18n';
import ProfilePage from './ProfilePage';
+import loadingApp from './__mocks__/loadingApp.mockStore';
+import viewOwnProfile from './__mocks__/viewOwnProfile.mockStore';
+import viewOtherProfile from './__mocks__/viewOtherProfile.mockStore';
+import invalidUser from './__mocks__/invalidUser.mockStore';
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useNavigate: jest.fn(),
+}));
const mockStore = configureMockStore([thunk]);
+
const storeMocks = {
- loadingApp: require('./__mocks__/loadingApp.mockStore'),
- invalidUser: require('./__mocks__/invalidUser.mockStore'),
- viewOwnProfile: require('./__mocks__/viewOwnProfile.mockStore'),
- viewOtherProfile: require('./__mocks__/viewOtherProfile.mockStore'),
- savingEditedBio: require('./__mocks__/savingEditedBio.mockStore'),
+ loadingApp,
+ viewOwnProfile,
+ viewOtherProfile,
+ invalidUser,
};
+
const requiredProfilePageProps = {
- fetchUserAccount: () => {},
- fetchProfile: () => {},
- saveProfile: () => {},
- saveProfilePhoto: () => {},
- deleteProfilePhoto: () => {},
- openField: () => {},
- closeField: () => {},
params: { username: 'staff' },
};
-// Mock language cookie
Object.defineProperty(global.document, 'cookie', {
writable: true,
value: `${getConfig().LANGUAGE_PREFERENCE_COOKIE_NAME}=en`,
@@ -65,54 +71,39 @@ configureI18n({
beforeEach(() => {
analytics.sendTrackingLogEvent.mockReset();
+ useNavigate.mockReset();
});
-const ProfileWrapper = ({ params, requiresParentalConsent }) => {
- const navigate = useNavigate();
- return (
-
- );
-};
-
-ProfileWrapper.propTypes = {
- params: PropTypes.shape({}).isRequired,
- requiresParentalConsent: PropTypes.bool.isRequired,
-};
-
const ProfilePageWrapper = ({
- contextValue, store, params, requiresParentalConsent,
+ contextValue, store, params,
}) => (
-
+
-
-
-
+
+
+ }
+ />
+
+
);
ProfilePageWrapper.defaultProps = {
+ // eslint-disable-next-line react/default-props-match-prop-types
params: { username: 'staff' },
- requiresParentalConsent: null,
};
ProfilePageWrapper.propTypes = {
contextValue: PropTypes.shape({}).isRequired,
store: PropTypes.shape({}).isRequired,
- params: PropTypes.shape({}),
- requiresParentalConsent: PropTypes.bool,
+ params: PropTypes.shape({
+ username: PropTypes.string.isRequired,
+ }).isRequired,
};
describe(' ', () => {
@@ -122,17 +113,12 @@ describe(' ', () => {
authenticatedUser: { userId: null, username: null, administrator: false },
config: getConfig(),
};
- const component = ;
- const { container: tree } = render(component);
- expect(tree).toMatchSnapshot();
- });
-
- it('successfully redirected to not found page.', () => {
- const contextValue = {
- authenticatedUser: { userId: 123, username: 'staff', administrator: true },
- config: getConfig(),
- };
- const component = ;
+ const component = (
+
+ );
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
@@ -142,7 +128,12 @@ describe(' ', () => {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
- const component = ;
+ const component = (
+
+ );
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
@@ -152,7 +143,6 @@ describe(' ', () => {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
-
const component = (
', () => {
...storeMocks.viewOtherProfile.profilePage,
account: {
...storeMocks.viewOtherProfile.profilePage.account,
- name: 'user',
- country: 'EN',
- bio: 'bio',
- courseCertificates: ['course certificates'],
- levelOfEducation: 'some level',
- languageProficiencies: ['some lang'],
- socialLinks: ['twitter'],
- timeZone: 'time zone',
- accountPrivacy: 'all_users',
+ name: 'Verified User',
+ country: 'US',
+ bio: 'About me',
+ courseCertificates: [{ title: 'Course 1' }],
+ levelOfEducation: 'bachelors',
+ languageProficiencies: [{ code: 'en' }],
+ socialLinks: [{ platform: 'twitter', socialLink: 'https://twitter.com/user' }],
+ },
+ preferences: {
+ ...storeMocks.viewOtherProfile.profilePage.preferences,
+ visibilityName: 'all_users',
+ visibilityCountry: 'all_users',
+ visibilityLevelOfEducation: 'all_users',
+ visibilityLanguageProficiencies: 'all_users',
+ visibilitySocialLinks: 'all_users',
+ visibilityBio: 'all_users',
},
},
})}
- match={{ params: { username: 'verified' } }} // Override default match
- />
- );
- const { container: tree } = render(component);
- expect(tree).toMatchSnapshot();
- });
-
- it('while saving an edited bio', () => {
- const contextValue = {
- authenticatedUser: { userId: 123, username: 'staff', administrator: true },
- config: getConfig(),
- };
- const component = (
-
- );
- const { container: tree } = render(component);
- expect(tree).toMatchSnapshot();
- });
-
- it('while saving an edited bio with error', () => {
- const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
- storeData.profilePage.errors.bio = { userMessage: 'bio error' };
- const contextValue = {
- authenticatedUser: { userId: 123, username: 'staff', administrator: true },
- config: getConfig(),
- };
- const component = (
-
- );
- const { container: tree } = render(component);
- expect(tree).toMatchSnapshot();
- });
-
- it('test country edit with error', () => {
- const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
- storeData.profilePage.errors.country = { userMessage: 'country error' };
- storeData.profilePage.currentlyEditingField = 'country';
- const contextValue = {
- authenticatedUser: { userId: 123, username: 'staff', administrator: true },
- config: getConfig(),
- };
- const component = (
-
- );
- const { container: tree } = render(component);
- expect(tree).toMatchSnapshot();
- });
-
- it('test education edit with error', () => {
- const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
- storeData.profilePage.errors.levelOfEducation = { userMessage: 'education error' };
- storeData.profilePage.currentlyEditingField = 'levelOfEducation';
- const contextValue = {
- authenticatedUser: { userId: 123, username: 'staff', administrator: true },
- config: getConfig(),
- };
- const component = (
-
- );
- const { container: tree } = render(component);
- expect(tree).toMatchSnapshot();
- });
-
- it('test preferreded language edit with error', () => {
- const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
- storeData.profilePage.errors.languageProficiencies = { userMessage: 'preferred language error' };
- storeData.profilePage.currentlyEditingField = 'languageProficiencies';
- const contextValue = {
- authenticatedUser: { userId: 123, username: 'staff', administrator: true },
- config: getConfig(),
- };
- const component = (
-
);
const { container: tree } = render(component);
@@ -284,63 +195,24 @@ describe(' ', () => {
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
- it('test age message alert', () => {
- const storeData = JSON.parse(JSON.stringify(storeMocks.viewOwnProfile));
- storeData.userAccount.requiresParentalConsent = true;
- storeData.profilePage.account.requiresParentalConsent = true;
- const contextValue = {
- authenticatedUser: { userId: 123, username: 'staff', administrator: true },
- config: { ...getConfig(), COLLECT_YEAR_OF_BIRTH: true },
- };
- const { container } = render(
- ,
- );
- expect(container.querySelector('.alert-info')).toHaveClass('show');
- });
- it('test photo error alert', () => {
- const storeData = JSON.parse(JSON.stringify(storeMocks.viewOwnProfile));
- storeData.profilePage.errors.photo = { userMessage: 'error' };
- const contextValue = {
- authenticatedUser: { userId: 123, username: 'staff', administrator: true },
- config: { ...getConfig(), COLLECT_YEAR_OF_BIRTH: true },
- };
- const { container } = render(
- ,
- );
-
- expect(container.querySelector('.alert-danger')).toHaveClass('show');
- });
-
- it.each([
- ['test user with non-disabled country', 'PK'],
- ['test user with disabled country', 'RU'],
- ])('test user with %s', (_, accountCountry) => {
- const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
- storeData.profilePage.errors.country = {};
- storeData.profilePage.currentlyEditingField = 'country';
- storeData.profilePage.disabledCountries = ['RU'];
- storeData.profilePage.account.country = accountCountry;
+ it('successfully redirected to not found page', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
+ const navigate = jest.fn();
+ useNavigate.mockReturnValue(navigate);
const component = (
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
+ expect(navigate).toHaveBeenCalledWith('/notfound');
});
});
@@ -358,11 +230,30 @@ describe(' ', () => {
/>,
);
- expect(analytics.sendTrackingLogEvent.mock.calls.length).toBe(1);
- expect(analytics.sendTrackingLogEvent.mock.calls[0][0]).toEqual('edx.profile.viewed');
- expect(analytics.sendTrackingLogEvent.mock.calls[0][1]).toEqual({
+ expect(analytics.sendTrackingLogEvent).toHaveBeenCalledTimes(1);
+ expect(analytics.sendTrackingLogEvent).toHaveBeenCalledWith('edx.profile.viewed', {
username: 'test-username',
});
});
});
+
+ describe('handles navigation', () => {
+ it('navigates to notfound on save error with no username', () => {
+ const contextValue = {
+ authenticatedUser: { userId: 123, username: 'staff', administrator: true },
+ config: getConfig(),
+ };
+ const navigate = jest.fn();
+ useNavigate.mockReturnValue(navigate);
+ render(
+ ,
+ );
+
+ expect(navigate).toHaveBeenCalledWith('/notfound');
+ });
+ });
});
diff --git a/src/profile-v2/UserCertificateSummary.jsx b/src/profile/UserCertificateSummary.jsx
similarity index 100%
rename from src/profile-v2/UserCertificateSummary.jsx
rename to src/profile/UserCertificateSummary.jsx
diff --git a/src/profile/UsernameDescription.jsx b/src/profile/UsernameDescription.jsx
deleted file mode 100644
index 684eb71..0000000
--- a/src/profile/UsernameDescription.jsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from 'react';
-import { FormattedMessage } from '@edx/frontend-platform/i18n';
-import { VisibilityOff } from '@openedx/paragon/icons';
-import { Icon } from '@openedx/paragon';
-import { getConfig } from '@edx/frontend-platform';
-
-const UsernameDescription = () => (
-
-);
-
-export default UsernameDescription;
diff --git a/src/profile/__mocks__/invalidUser.mockStore.js b/src/profile/__mocks__/invalidUser.mockStore.js
index 8643a21..253ef87 100644
--- a/src/profile/__mocks__/invalidUser.mockStore.js
+++ b/src/profile/__mocks__/invalidUser.mockStore.js
@@ -29,7 +29,7 @@ module.exports = {
drafts: {},
isLoadingProfile: false,
isAuthenticatedUserProfile: true,
- countriesCodesList: [],
+ countriesCodesList: ['US', 'CA', 'GB', 'ME']
},
router: {
location: {
diff --git a/src/profile/__mocks__/loadingApp.mockStore.js b/src/profile/__mocks__/loadingApp.mockStore.js
index dfef507..aaf1f63 100644
--- a/src/profile/__mocks__/loadingApp.mockStore.js
+++ b/src/profile/__mocks__/loadingApp.mockStore.js
@@ -29,6 +29,7 @@ module.exports = {
drafts: {},
isLoadingProfile: true,
isAuthenticatedUserProfile: true,
+ countriesCodesList: ['US', 'CA', 'GB', 'ME']
},
router: {
location: {
diff --git a/src/profile/__mocks__/savingEditedBio.mockStore.js b/src/profile/__mocks__/savingEditedBio.mockStore.js
index 7ba9a52..a104762 100644
--- a/src/profile/__mocks__/savingEditedBio.mockStore.js
+++ b/src/profile/__mocks__/savingEditedBio.mockStore.js
@@ -126,7 +126,7 @@ module.exports = {
],
drafts: {},
isLoadingProfile: false,
- countriesCodesList: [],
+ disabledCountries: [],
},
router: {
location: {
diff --git a/src/profile/__mocks__/viewOtherProfile.mockStore.js b/src/profile/__mocks__/viewOtherProfile.mockStore.js
index b35f61d..7afdac1 100644
--- a/src/profile/__mocks__/viewOtherProfile.mockStore.js
+++ b/src/profile/__mocks__/viewOtherProfile.mockStore.js
@@ -81,12 +81,18 @@ module.exports = {
gender: null,
accountPrivacy: 'private'
},
- preferences: {},
+ preferences: {
+ visibilityName: 'all_users',
+ visibilityCountry: 'all_users',
+ visibilityLevelOfEducation: 'all_users',
+ visibilityLanguageProficiencies: 'all_users',
+ visibilitySocialLinks: 'all_users',
+ visibilityBio: 'all_users'
+ },
courseCertificates: [],
drafts: {},
isLoadingProfile: false,
- learningGoal: 'advance_career',
- countriesCodesList: [],
+ countriesCodesList: ['US', 'CA', 'GB', 'ME']
},
router: {
location: {
diff --git a/src/profile/__mocks__/viewOwnProfile.mockStore.js b/src/profile/__mocks__/viewOwnProfile.mockStore.js
index b40d8f0..4fb2510 100644
--- a/src/profile/__mocks__/viewOwnProfile.mockStore.js
+++ b/src/profile/__mocks__/viewOwnProfile.mockStore.js
@@ -124,9 +124,9 @@ module.exports = {
createdDate: '2019-03-04T19:31:39.896806Z'
}
],
- countriesCodesList:[{code:"AX"},{code:"AL"}],
drafts: {},
- isLoadingProfile: false
+ isLoadingProfile: false,
+ countriesCodesList: ['US', 'CA', 'GB', 'ME']
},
router: {
location: {
diff --git a/src/profile/__snapshots__/ProfilePage.test.jsx.snap b/src/profile/__snapshots__/ProfilePage.test.jsx.snap
index 455ae82..367f0f8 100644
--- a/src/profile/__snapshots__/ProfilePage.test.jsx.snap
+++ b/src/profile/__snapshots__/ProfilePage.test.jsx.snap
@@ -5,13 +5,9 @@ exports[` Renders correctly in various states app loading 1`] = `
-
Renders correctly in various states app loading 1`] = `
`;
-exports[`
Renders correctly in various states successfully redirected to not found page. 1`] = `
+exports[`
Renders correctly in various states successfully redirected to not found page 1`] = `
-
Renders correctly in various states successfully redire
-
Renders correctly in various states successfully redire
/>
-
-
-
-
-
-
- staff
-
+ staffTest
+
-
-
-
-
-
-
- Your profile information is only visible to you. Only your username is visible to others on localhost.
-
-
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
-
-
-
-
-
-
- Your profile information is only visible to you. Only your username is visible to others on localhost.
-
-
-
-
-
-
-
-
-
-
-
- This is the name that appears in your account and on your certificates.
-
-
-
-
-
-
-
-
-
- Add location
-
-
-
-
-
-
-
-
-
-
-
-
- Add language
-
-
-
-
-
-
-
-
-
-
-
-
- Add education
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add Twitter
-
-
-
-
-
-
-
-
-
- Add Facebook
-
-
-
-
-
-
-
-
-
- Add LinkedIn
-
-
-
-
-
+ class="p-0 col-auto"
+ />
-
-
-
-
-
-
-
-
- Add a short bio
-
-
-
-
-
-
-
- You don't have any certificates yet.
-
-
-
+ class="ml-auto"
+ />
-
-
-`;
-
-exports[`
Renders correctly in various states test country edit with error 1`] = `
-
-
-
+
+ Profile information
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
- Lemon Seltzer
-
-
- This is the name that appears in your account and on your certificates.
-
+ staffTest
+
-
-
-
-
-
- Elementary/primary school
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Verified Certificate
-
-
- edX Demonstration Course
-
-
-
- From
-
-
- edX
-
-
-
- Completed on
- 3/4/2019
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`
Renders correctly in various states test education edit with error 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
- Lemon Seltzer
-
-
- This is the name that appears in your account and on your certificates.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Verified Certificate
-
-
- edX Demonstration Course
-
-
-
- From
-
-
- edX
-
-
-
- Completed on
- 3/4/2019
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`
Renders correctly in various states test preferreded language edit with error 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
- Lemon Seltzer
-
-
- This is the name that appears in your account and on your certificates.
-
-
-
-
-
-
-
-
-
- Elementary/primary school
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Verified Certificate
-
-
- edX Demonstration Course
-
-
-
- From
-
-
- edX
-
-
-
- Completed on
- 3/4/2019
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`
Renders correctly in various states test user with test user with disabled country 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
- Lemon Seltzer
-
-
- This is the name that appears in your account and on your certificates.
-
-
-
-
-
-
-
-
-
- Elementary/primary school
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Verified Certificate
-
-
- edX Demonstration Course
-
-
-
- From
-
-
- edX
-
-
-
- Completed on
- 3/4/2019
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`
Renders correctly in various states test user with test user with non-disabled country 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
- Lemon Seltzer
-
-
- This is the name that appears in your account and on your certificates.
-
-
-
-
-
-
-
-
-
- Elementary/primary school
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Verified Certificate
-
-
- edX Demonstration Course
-
-
-
- From
-
-
- edX
-
-
-
- Completed on
- 3/4/2019
-
-
-
-
-
-
-
-
-
-
-
`;
@@ -6247,19 +168,16 @@ exports[`
Renders correctly in various states viewing other profi
class="profile-page"
>
-
Renders correctly in various states viewing other profi
/>
+
+
+ verified
+
+
+ Verified User
+
+
+
+ Member since
+
+
+ 2017
+
+
+
+
+
+
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
- Your profile information is only visible to you. Only your username is visible to others on localhost.
-
-
-
-
-
-
-
+ class="ml-auto"
+ />
+
+
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
- Your profile information is only visible to you. Only your username is visible to others on localhost.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Other education
-
-
-
-
-
+ Profile information
+
+
+
+
+
+
+ Primary language spoken
+
+
+
+
+
+
+
+
+
- You don't have any certificates yet.
+
`;
@@ -6653,19 +547,16 @@ exports[`
Renders correctly in various states viewing own profile
class="profile-page"
>
-
Renders correctly in various states viewing own profile
-
+
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
+ staff
+
+
Lemon Seltzer
-
- This is the name that appears in your account and on your certificates.
-
-
-
-
-
-
-
-
-
- Elementary/primary school
-
-
-
-
-
-
-
+ 1
+
+ certifications
+
+
+
+
+
+
+
+
+
+
+ Profile information
+
+
+
+
+
-
- This is my bio
-
+ staff
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Just me
+
+
+
+
+
+
+
+
+ Country
+
+
+
+
+
+
+
+
+
+ Everyone on localhost
+
+
+
+
+
+
+
+
+ Primary language spoken
+
+
+
+
+
+
+
+
+
+ Everyone on localhost
+
+
+
+
+
+
+
+
+ Education
+
+
+
+
+
+
+
+
+
+ Just me
+
+
+
+
-
-
-
-
+
+
+
+
-
- Verified Certificate
-
-
- edX Demonstration Course
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ Everyone on localhost
+
+
+
+
+
+
+
+
+
+
- From
-
-
- edX
+ X
-
- Completed on
- 3/4/2019
-
-
+
+
+
+
+
+
- in a new tab
-
+
+
+
+ Everyone on localhost
-
-
+
+
+
+
+
+
+ Facebook
+
+
+
+
+
+
+
+
+
+
+ Everyone on localhost
+
+
+
+
+
+
+
+ LinkedIn
+
+
+
+
+
+
+ Add
+ LinkedIn
+
@@ -7520,1889 +1363,98 @@ exports[`
Renders correctly in various states viewing own profile
-
-
-`;
-
-exports[`
Renders correctly in various states while saving an edited bio 1`] = `
-
-
-
-
+
-
-
-
-
-
-
-
+ Your certificates
+
+
+
+
+ Your learner records information is only visible to you. Only your username and profile image are visible to others on localhost.
+
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
- Lemon Seltzer
-
-
- This is the name that appears in your account and on your certificates.
-
-
-
-
-
-
-
-
-
- Elementary/primary school
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Verified Certificate
-
-
- edX Demonstration Course
-
-
-
- From
-
-
- edX
-
-
-
- Completed on
- 3/4/2019
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`
Renders correctly in various states while saving an edited bio with error 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
- Lemon Seltzer
-
-
- This is the name that appears in your account and on your certificates.
-
-
-
-
-
-
-
-
-
- Elementary/primary school
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Verified Certificate
-
-
- edX Demonstration Course
-
-
-
- From
-
-
- edX
-
-
-
- Completed on
- 3/4/2019
-
-
-
+ Verified Certificate
+
+
+ edX Demonstration Course
+
+
+ From
+
+
+ edX
+
+
+ Completed on
+ 3/4/2019
+
+
+
+ Credential ID
+
@@ -9420,19 +1472,16 @@ exports[`
Renders correctly in various states without credentials
class="profile-page"
>
-
Renders correctly in various states without credentials
-
+
@@ -9477,726 +1552,723 @@ exports[` Renders correctly in various states without credentials
/>
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
-
-
-
-
-
- staff
-
-
- Member since
- 2017
-
-
-
-
-
-
-
+ staff
+
+
Lemon Seltzer
-
- This is the name that appears in your account and on your certificates.
-
-
-
-
-
-
-
-
-
- Elementary/primary school
-
-
-
-
-
-
-
+ 1
+
+ certifications
+
+
+
+
+
+
+
+
+
+ Profile information
+
+
+
+
+
-
- This is my bio
-
+ staff
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Just me
+
+
+
+
+
+
+
+
+ Country
+
+
+
+
+
+
+
+
+
+ Everyone on localhost
+
+
+
+
+
+
+
+
+ Primary language spoken
+
+
+
+
+
+
+
+
+
+ Everyone on localhost
+
+
+
+
+
+
+
+
+ Education
+
+
+
+
+
+
+
+
+
+ Just me
+
+
+
+
-
-
-
-
+
+
+
+
-
- Verified Certificate
-
-
- edX Demonstration Course
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ Everyone on localhost
+
+
+
+
+
+
+
+
+
+
- From
-
-
- edX
+ X
-
- Completed on
- 3/4/2019
-
-
+
+
+
+
+
+
- in a new tab
-
+
+
+
+ Everyone on localhost
-
-
+
+
+
+
+
+
+ Facebook
+
+
+
+
+
+
+
+
+
+
+ Everyone on localhost
+
+
+
+
+
+
+
+ LinkedIn
+
+
+
+
+
+
+ Add
+ LinkedIn
+
@@ -10207,6 +2279,105 @@ exports[`
Renders correctly in various states without credentials
+
+
+
+
+
+ Your certificates
+
+
+
+
+ Your learner records information is only visible to you. Only your username and profile image are visible to others on localhost.
+
+
+
+
+
+
+
+
+
+
+
+ Verified Certificate
+
+
+ edX Demonstration Course
+
+
+ From
+
+
+ edX
+
+
+ Completed on
+ 3/4/2019
+
+
+
+
+ Credential ID
+
+
+
+
+
+
+
+
`;
diff --git a/src/profile/data/actions.js b/src/profile/data/actions.js
index 85edb5b..b960400 100644
--- a/src/profile/data/actions.js
+++ b/src/profile/data/actions.js
@@ -9,8 +9,6 @@ export const CLOSE_FORM = 'CLOSE_FORM';
export const UPDATE_DRAFT = 'UPDATE_DRAFT';
export const RESET_DRAFTS = 'RESET_DRAFTS';
-// FETCH PROFILE ACTIONS
-
export const fetchProfile = username => ({
type: FETCH_PROFILE.BASE,
payload: { username },
@@ -25,22 +23,18 @@ export const fetchProfileSuccess = (
preferences,
courseCertificates,
isAuthenticatedUserProfile,
- countriesCodesList,
) => ({
type: FETCH_PROFILE.SUCCESS,
account,
preferences,
courseCertificates,
isAuthenticatedUserProfile,
- countriesCodesList,
});
export const fetchProfileReset = () => ({
type: FETCH_PROFILE.RESET,
});
-// SAVE PROFILE ACTIONS
-
export const saveProfile = (formId, username) => ({
type: SAVE_PROFILE.BASE,
payload: {
@@ -70,8 +64,6 @@ export const saveProfileFailure = errors => ({
payload: { errors },
});
-// SAVE PROFILE PHOTO ACTIONS
-
export const saveProfilePhoto = (username, formData) => ({
type: SAVE_PROFILE_PHOTO.BASE,
payload: {
@@ -98,8 +90,6 @@ export const saveProfilePhotoFailure = error => ({
payload: { error },
});
-// DELETE PROFILE PHOTO ACTIONS
-
export const deleteProfilePhoto = username => ({
type: DELETE_PROFILE_PHOTO.BASE,
payload: {
@@ -120,8 +110,6 @@ export const deleteProfilePhotoReset = () => ({
type: DELETE_PROFILE_PHOTO.RESET,
});
-// FIELD STATE ACTIONS
-
export const openForm = formId => ({
type: OPEN_FORM,
payload: {
@@ -136,8 +124,6 @@ export const closeForm = formId => ({
},
});
-// FORM STATE ACTIONS
-
export const updateDraft = (name, value) => ({
type: UPDATE_DRAFT,
payload: {
diff --git a/src/profile/data/actions.test.js b/src/profile/data/actions.test.js
index 6268888..275d695 100644
--- a/src/profile/data/actions.test.js
+++ b/src/profile/data/actions.test.js
@@ -1,14 +1,4 @@
import {
- openForm,
- closeForm,
- OPEN_FORM,
- CLOSE_FORM,
- SAVE_PROFILE,
- saveProfileBegin,
- saveProfileSuccess,
- saveProfileFailure,
- saveProfileReset,
- saveProfile,
SAVE_PROFILE_PHOTO,
saveProfilePhotoBegin,
saveProfilePhotoSuccess,
@@ -22,76 +12,6 @@ import {
deleteProfilePhoto,
} from './actions';
-describe('editable field actions', () => {
- it('should create an open action', () => {
- const expectedAction = {
- type: OPEN_FORM,
- payload: {
- formId: 'name',
- },
- };
- expect(openForm('name')).toEqual(expectedAction);
- });
-
- it('should create a closed action', () => {
- const expectedAction = {
- type: CLOSE_FORM,
- payload: {
- formId: 'name',
- },
- };
- expect(closeForm('name')).toEqual(expectedAction);
- });
-});
-
-describe('SAVE profile actions', () => {
- it('should create an action to signal the start of a profile save', () => {
- const expectedAction = {
- type: SAVE_PROFILE.BASE,
- payload: {
- formId: 'name',
- },
- };
- expect(saveProfile('name')).toEqual(expectedAction);
- });
-
- it('should create an action to signal user profile save success', () => {
- const accountData = { name: 'Full Name' };
- const preferencesData = { visibility: { name: 'private' } };
- const expectedAction = {
- type: SAVE_PROFILE.SUCCESS,
- payload: {
- account: accountData,
- preferences: preferencesData,
- },
- };
- expect(saveProfileSuccess(accountData, preferencesData)).toEqual(expectedAction);
- });
-
- it('should create an action to signal user profile save beginning', () => {
- const expectedAction = {
- type: SAVE_PROFILE.BEGIN,
- };
- expect(saveProfileBegin()).toEqual(expectedAction);
- });
-
- it('should create an action to signal user profile save success', () => {
- const expectedAction = {
- type: SAVE_PROFILE.RESET,
- };
- expect(saveProfileReset()).toEqual(expectedAction);
- });
-
- it('should create an action to signal user account save failure', () => {
- const errors = ['Test failure'];
- const expectedAction = {
- type: SAVE_PROFILE.FAILURE,
- payload: { errors },
- };
- expect(saveProfileFailure(errors)).toEqual(expectedAction);
- });
-});
-
describe('SAVE profile photo actions', () => {
it('should create an action to signal the start of a profile photo save', () => {
const formData = 'multipart form data';
@@ -123,7 +43,7 @@ describe('SAVE profile photo actions', () => {
expect(saveProfilePhotoSuccess(newPhotoData)).toEqual(expectedAction);
});
- it('should create an action to signal user profile photo save success', () => {
+ it('should create an action to signal user profile photo save reset', () => {
const expectedAction = {
type: SAVE_PROFILE_PHOTO.RESET,
};
@@ -169,34 +89,10 @@ describe('DELETE profile photo actions', () => {
expect(deleteProfilePhotoSuccess(defaultPhotoData)).toEqual(expectedAction);
});
- it('should create an action to signal user profile photo deletion success', () => {
+ it('should create an action to signal user profile photo deletion reset', () => {
const expectedAction = {
type: DELETE_PROFILE_PHOTO.RESET,
};
expect(deleteProfilePhotoReset()).toEqual(expectedAction);
});
});
-
-describe('Editable field opening and closing actions', () => {
- const formId = 'name';
-
- it('should create an action to signal the opening a field', () => {
- const expectedAction = {
- type: OPEN_FORM,
- payload: {
- formId,
- },
- };
- expect(openForm(formId)).toEqual(expectedAction);
- });
-
- it('should create an action to signal the closing a field', () => {
- const expectedAction = {
- type: CLOSE_FORM,
- payload: {
- formId,
- },
- };
- expect(closeForm(formId)).toEqual(expectedAction);
- });
-});
diff --git a/src/profile-v2/data/hooks.js b/src/profile/data/hooks.js
similarity index 93%
rename from src/profile-v2/data/hooks.js
rename to src/profile/data/hooks.js
index 161a436..abf37c5 100644
--- a/src/profile-v2/data/hooks.js
+++ b/src/profile/data/hooks.js
@@ -12,7 +12,7 @@ export function useIsOnMobileScreen() {
}
export function useIsVisibilityEnabled() {
- return getConfig().DISABLE_VISIBILITY_EDITING === 'true';
+ return getConfig().DISABLE_VISIBILITY_EDITING !== 'true';
}
export function useHandleChange(changeHandler) {
diff --git a/src/profile/data/pact-profile.test.js b/src/profile/data/pact-profile.test.js
index 3addaa4..abd14e6 100644
--- a/src/profile/data/pact-profile.test.js
+++ b/src/profile/data/pact-profile.test.js
@@ -17,6 +17,10 @@ const expectedUserInfo200 = {
dateJoined: '2017-06-07T00:44:23Z',
isActive: true,
yearOfBirth: 1901,
+ languageProficiencies: [],
+ levelOfEducation: null,
+ profileImage: {},
+ socialLinks: [],
};
const provider = new PactV3({
diff --git a/src/profile/data/reducers.js b/src/profile/data/reducers.js
index 0d374c6..3e4760c 100644
--- a/src/profile/data/reducers.js
+++ b/src/profile/data/reducers.js
@@ -16,12 +16,27 @@ export const initialState = {
currentlyEditingField: null,
account: {
socialLinks: [],
+ languageProficiencies: [],
+ name: '',
+ bio: '',
+ country: '',
+ levelOfEducation: '',
+ profileImage: {},
+ yearOfBirth: '',
+ },
+ preferences: {
+ visibilityName: '',
+ visibilityBio: '',
+ visibilityCountry: '',
+ visibilityLevelOfEducation: '',
+ visibilitySocialLinks: '',
+ visibilityLanguageProficiencies: '',
},
- preferences: {},
courseCertificates: [],
drafts: {},
isLoadingProfile: true,
isAuthenticatedUserProfile: false,
+ disabledCountries: ['RU'],
countriesCodesList: [],
};
@@ -38,12 +53,17 @@ const profilePage = (state = initialState, action = {}) => {
case FETCH_PROFILE.SUCCESS:
return {
...state,
- account: action.account,
+ account: {
+ ...state.account,
+ ...action.account,
+ socialLinks: action.account.socialLinks || [],
+ languageProficiencies: action.account.languageProficiencies || [],
+ },
preferences: action.preferences,
- courseCertificates: action.courseCertificates,
+ courseCertificates: action.courseCertificates || [],
isLoadingProfile: false,
isAuthenticatedUserProfile: action.isAuthenticatedUserProfile,
- countriesCodesList: action.countriesCodesList,
+ countriesCodesList: action.countriesCodesList || [],
};
case SAVE_PROFILE.BEGIN:
return {
@@ -56,9 +76,12 @@ const profilePage = (state = initialState, action = {}) => {
...state,
saveState: 'complete',
errors: {},
- // Account is always replaced completely.
- account: action.payload.account !== null ? action.payload.account : state.account,
- // Preferences changes get merged in.
+ account: action.payload.account !== null ? {
+ ...state.account,
+ ...action.payload.account,
+ socialLinks: action.payload.account.socialLinks || [],
+ languageProficiencies: action.payload.account.languageProficiencies || [],
+ } : state.account,
preferences: { ...state.preferences, ...action.payload.preferences },
};
case SAVE_PROFILE.FAILURE:
@@ -75,7 +98,6 @@ const profilePage = (state = initialState, action = {}) => {
isLoadingProfile: false,
errors: {},
};
-
case SAVE_PROFILE_PHOTO.BEGIN:
return {
...state,
@@ -85,7 +107,6 @@ const profilePage = (state = initialState, action = {}) => {
case SAVE_PROFILE_PHOTO.SUCCESS:
return {
...state,
- // Merge in new profile image data
account: { ...state.account, profileImage: action.payload.profileImage },
savePhotoState: 'complete',
errors: {},
@@ -102,7 +123,6 @@ const profilePage = (state = initialState, action = {}) => {
savePhotoState: null,
errors: {},
};
-
case DELETE_PROFILE_PHOTO.BEGIN:
return {
...state,
@@ -112,7 +132,6 @@ const profilePage = (state = initialState, action = {}) => {
case DELETE_PROFILE_PHOTO.SUCCESS:
return {
...state,
- // Merge in new profile image data (should be empty or default image)
account: { ...state.account, profileImage: action.payload.profileImage },
savePhotoState: 'complete',
errors: {},
@@ -129,13 +148,11 @@ const profilePage = (state = initialState, action = {}) => {
savePhotoState: null,
errors: {},
};
-
case UPDATE_DRAFT:
return {
...state,
drafts: { ...state.drafts, [action.payload.name]: action.payload.value },
};
-
case RESET_DRAFTS:
return {
...state,
@@ -148,7 +165,6 @@ const profilePage = (state = initialState, action = {}) => {
drafts: {},
};
case CLOSE_FORM:
- // Only close if the field to close is undefined or matches the field that is currently open
if (action.payload.formId === state.currentlyEditingField) {
return {
...state,
diff --git a/src/profile-v2/data/reducers.test.js b/src/profile/data/reducers.test.js
similarity index 100%
rename from src/profile-v2/data/reducers.test.js
rename to src/profile/data/reducers.test.js
diff --git a/src/profile/data/sagas.js b/src/profile/data/sagas.js
index e2ebba5..b64c3fe 100644
--- a/src/profile/data/sagas.js
+++ b/src/profile/data/sagas.js
@@ -1,3 +1,4 @@
+import { history } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import pick from 'lodash.pick';
import {
@@ -21,13 +22,12 @@ import {
resetDrafts,
saveProfileBegin,
saveProfileFailure,
- saveProfilePhotoBegin,
- saveProfilePhotoFailure,
- saveProfilePhotoReset,
- saveProfilePhotoSuccess,
saveProfileReset,
saveProfileSuccess,
SAVE_PROFILE,
+ saveProfilePhotoBegin,
+ saveProfilePhotoReset,
+ saveProfilePhotoSuccess,
SAVE_PROFILE_PHOTO,
} from './actions';
import { handleSaveProfileSelector, userAccountSelector } from './selectors';
@@ -37,7 +37,6 @@ export function* handleFetchProfile(action) {
const { username } = action.payload;
const userAccount = yield select(userAccountSelector);
const isAuthenticatedUserProfile = username === getAuthenticatedUser().username;
- // Default our data assuming the account is the current user's account.
let preferences = {};
let account = userAccount;
let courseCertificates = null;
@@ -46,7 +45,6 @@ export function* handleFetchProfile(action) {
try {
yield put(fetchProfileBegin());
- // Depending on which profile we're loading, we need to make different calls.
const calls = [
call(ProfileApiService.getAccount, username),
call(ProfileApiService.getCourseCertificates, username),
@@ -54,12 +52,9 @@ export function* handleFetchProfile(action) {
];
if (isAuthenticatedUserProfile) {
- // If the profile is for the current user, get their preferences.
- // We don't need them for other users.
calls.push(call(ProfileApiService.getPreferences, username));
}
- // Make all the calls in parallel.
const result = yield all(calls);
if (isAuthenticatedUserProfile) {
@@ -68,9 +63,6 @@ export function* handleFetchProfile(action) {
[account, courseCertificates, countriesCodesList] = result;
}
- // Set initial visibility values for account
- // Set account_privacy as custom is necessary so that when viewing another user's profile,
- // their full name is displayed and change visibility forms are worked correctly
if (isAuthenticatedUserProfile && result[0].accountPrivacy === 'all_users') {
yield call(ProfileApiService.patchPreferences, action.payload.username, {
account_privacy: 'custom',
@@ -97,11 +89,7 @@ export function* handleFetchProfile(action) {
yield put(fetchProfileReset());
} catch (e) {
if (e.response.status === 404) {
- if (e.processedData && e.processedData.fieldErrors) {
- yield put(saveProfileFailure(e.processedData.fieldErrors));
- } else {
- yield put(saveProfileFailure(e.customAttributes));
- }
+ history.push('/notfound');
} else {
throw e;
}
@@ -114,7 +102,6 @@ export function* handleSaveProfile(action) {
const accountDrafts = pick(drafts, [
'bio',
- 'courseCertificates',
'country',
'levelOfEducation',
'languageProficiencies',
@@ -124,7 +111,6 @@ export function* handleSaveProfile(action) {
const preferencesDrafts = pick(drafts, [
'visibilityBio',
- 'visibilityCourseCertificates',
'visibilityCountry',
'visibilityLevelOfEducation',
'visibilityLanguageProficiencies',
@@ -138,7 +124,6 @@ export function* handleSaveProfile(action) {
yield put(saveProfileBegin());
let accountResult = null;
- // Build the visibility drafts into a structure the API expects.
if (Object.keys(accountDrafts).length > 0) {
accountResult = yield call(
@@ -148,17 +133,14 @@ export function* handleSaveProfile(action) {
);
}
- let preferencesResult = preferences; // assume it hasn't changed.
+ let preferencesResult = preferences;
if (Object.keys(preferencesDrafts).length > 0) {
yield call(ProfileApiService.patchPreferences, action.payload.username, preferencesDrafts);
// TODO: Temporary deoptimization since the patchPreferences call doesn't return anything.
- // Remove this second call once we can get a result from the one above.
+
preferencesResult = yield call(ProfileApiService.getPreferences, action.payload.username);
}
- // The account result is returned from the server.
- // The preferences draft is valid if the server didn't complain, so
- // pass it through directly.
yield put(saveProfileSuccess(accountResult, preferencesResult));
yield delay(1000);
yield put(closeForm(action.payload.formId));
@@ -184,12 +166,7 @@ export function* handleSaveProfilePhoto(action) {
yield put(saveProfilePhotoSuccess(photoResult));
yield put(saveProfilePhotoReset());
} catch (e) {
- if (e.processedData) {
- yield put(saveProfilePhotoFailure(e.processedData));
- } else {
- yield put(saveProfilePhotoReset());
- throw e;
- }
+ yield put(saveProfilePhotoReset());
}
}
@@ -203,7 +180,6 @@ export function* handleDeleteProfilePhoto(action) {
yield put(deleteProfilePhotoReset());
} catch (e) {
yield put(deleteProfilePhotoReset());
- throw e;
}
}
diff --git a/src/profile/data/sagas.test.js b/src/profile/data/sagas.test.js
index 291ab1b..2da09b3 100644
--- a/src/profile/data/sagas.test.js
+++ b/src/profile/data/sagas.test.js
@@ -26,7 +26,6 @@ jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(),
}));
-// RootSaga and ProfileApiService must be imported AFTER the mock above.
/* eslint-disable import/first */
import profileSaga, {
handleFetchProfile,
@@ -78,7 +77,6 @@ describe('RootSaga', () => {
call(ProfileApiService.getCourseCertificates, 'gonzo'),
call(ProfileApiService.getCountryList),
call(ProfileApiService.getPreferences, 'gonzo'),
-
]));
expect(gen.next(result).value)
.toEqual(put(profileActions.fetchProfileSuccess(userAccount, result[3], result[1], true, [])));
@@ -137,8 +135,6 @@ describe('RootSaga', () => {
expect(gen.next().value).toEqual(call(ProfileApiService.patchProfile, 'my username', {
name: 'Full Name',
}));
- // The library would supply the result of the above call
- // as the parameter to the NEXT yield. Here:
expect(gen.next(profile).value).toEqual(put(profileActions.saveProfileSuccess(profile, {})));
expect(gen.next().value).toEqual(delay(1000));
expect(gen.next().value).toEqual(put(profileActions.closeForm('ze form id')));
diff --git a/src/profile/data/selectors.js b/src/profile/data/selectors.js
index 41d69fa..d398295 100644
--- a/src/profile/data/selectors.js
+++ b/src/profile/data/selectors.js
@@ -5,18 +5,15 @@ import {
getCountryList,
getCountryMessages,
getLanguageMessages,
-} from '@edx/frontend-platform/i18n'; // eslint-disable-line
+} from '@edx/frontend-platform/i18n';
export const formIdSelector = (state, props) => props.formId;
export const userAccountSelector = state => state.userAccount;
-
export const profileAccountSelector = state => state.profilePage.account;
export const profileDraftsSelector = state => state.profilePage.drafts;
export const accountPrivacySelector = state => state.profilePage.preferences.accountPrivacy;
export const profilePreferencesSelector = state => state.profilePage.preferences;
export const profileCourseCertificatesSelector = state => state.profilePage.courseCertificates;
-export const profileAccountDraftsSelector = state => state.profilePage.accountDrafts;
-export const profileVisibilityDraftsSelector = state => state.profilePage.visibilityDrafts;
export const saveStateSelector = state => state.profilePage.saveState;
export const savePhotoStateSelector = state => state.profilePage.savePhotoState;
export const isLoadingProfileSelector = state => state.profilePage.isLoadingProfile;
@@ -32,22 +29,11 @@ export const editableFormModeSelector = createSelector(
formIdSelector,
currentlyEditingFieldSelector,
(account, isAuthenticatedUserProfile, certificates, formId, currentlyEditingField) => {
- // If the prop doesn't exist, that means it hasn't been set (for the current user's profile)
- // or is being hidden from us (for other users' profiles)
let propExists = account[formId] != null && account[formId].length > 0;
- propExists = formId === 'certificates' ? certificates.length > 0 : propExists; // overwrite for certificates
- // If this isn't the current user's profile
+ propExists = formId === 'certificates' ? certificates.length > 0 : propExists;
if (!isAuthenticatedUserProfile) {
return 'static';
}
- // the current user has no age set / under 13 ...
- if (account.requiresParentalConsent) {
- // then there are only two options: static or nothing.
- // We use 'null' as a return value because the consumers of
- // getMode render nothing at all on a mode of null.
- return propExists ? 'static' : null;
- }
- // Otherwise, if this is the current user's profile...
if (formId === currentlyEditingField) {
return 'editing';
}
@@ -68,12 +54,10 @@ export const accountDraftsFieldSelector = createSelector(
export const visibilityDraftsFieldSelector = createSelector(
formIdSelector,
- profileVisibilityDraftsSelector,
- (formId, visibilityDrafts) => visibilityDrafts[formId],
+ profileDraftsSelector,
+ (formId, drafts) => drafts[`visibility${formId.charAt(0).toUpperCase() + formId.slice(1)}`],
);
-// Note: Error messages are delivered from the server
-// localized according to a user's account settings
export const formErrorSelector = createSelector(
accountErrorsSelector,
formIdSelector,
@@ -91,11 +75,6 @@ export const editableFormSelector = createSelector(
}),
);
-// Because this selector has no input selectors, it will only be evaluated once. This is fine
-// for now because we don't allow users to change the locale after page load.
-// Once we DO allow this, we should create an actual action which dispatches the locale into redux,
-// then we can modify this to get the locale from state rather than from getLocale() directly.
-// Once we do that, this will work as expected and be re-evaluated when the locale changes.
export const localeSelector = () => getLocale();
export const countryMessagesSelector = createSelector(
localeSelector,
@@ -169,9 +148,6 @@ export const profileImageSelector = createSelector(
: {}),
);
-/**
- * This is used by a saga to pull out data to process.
- */
export const handleSaveProfileSelector = createSelector(
profileDraftsSelector,
profilePreferencesSelector,
@@ -181,7 +157,6 @@ export const handleSaveProfileSelector = createSelector(
}),
);
-// Reformats the social links in a platform-keyed hash.
const socialLinksByPlatformSelector = createSelector(
profileAccountSelector,
(account) => {
@@ -208,24 +183,18 @@ const draftSocialLinksByPlatformSelector = createSelector(
},
);
-// Fleshes out our list of existing social links with all the other ones the user can set.
export const formSocialLinksSelector = createSelector(
socialLinksByPlatformSelector,
draftSocialLinksByPlatformSelector,
(linksByPlatform, draftLinksByPlatform) => {
const knownPlatforms = ['twitter', 'facebook', 'linkedin'];
const socialLinks = [];
- // For each known platform
knownPlatforms.forEach((platform) => {
- // If the link is in our drafts.
if (draftLinksByPlatform[platform] !== undefined) {
- // Use the draft one.
socialLinks.push(draftLinksByPlatform[platform]);
} else if (linksByPlatform[platform] !== undefined) {
- // Otherwise use the real one.
socialLinks.push(linksByPlatform[platform]);
} else {
- // And if it's not in either, use a stub.
socialLinks.push({
platform,
socialLink: null,
@@ -244,7 +213,6 @@ export const visibilitiesSelector = createSelector(
case 'custom':
return {
visibilityBio: preferences.visibilityBio || 'all_users',
- visibilityCourseCertificates: preferences.visibilityCourseCertificates || 'all_users',
visibilityCountry: preferences.visibilityCountry || 'all_users',
visibilityLevelOfEducation: preferences.visibilityLevelOfEducation || 'all_users',
visibilityLanguageProficiencies: preferences.visibilityLanguageProficiencies || 'all_users',
@@ -254,7 +222,6 @@ export const visibilitiesSelector = createSelector(
case 'private':
return {
visibilityBio: 'private',
- visibilityCourseCertificates: 'private',
visibilityCountry: 'private',
visibilityLevelOfEducation: 'private',
visibilityLanguageProficiencies: 'private',
@@ -263,13 +230,8 @@ export const visibilitiesSelector = createSelector(
};
case 'all_users':
default:
- // All users is intended to fall through to default.
- // If there is no value for accountPrivacy in perferences, that means it has not been
- // explicitly set yet. The server assumes - today - that this means "all_users",
- // so we emulate that here in the client.
return {
visibilityBio: 'all_users',
- visibilityCourseCertificates: 'all_users',
visibilityCountry: 'all_users',
visibilityLevelOfEducation: 'all_users',
visibilityLanguageProficiencies: 'all_users',
@@ -280,9 +242,6 @@ export const visibilitiesSelector = createSelector(
},
);
-/**
- * If there's no draft present at all (undefined), use the original committed value.
- */
function chooseFormValue(draft, committed) {
return draft !== undefined ? draft : committed;
}
@@ -297,10 +256,6 @@ export const formValuesSelector = createSelector(
bio: chooseFormValue(drafts.bio, account.bio),
visibilityBio: chooseFormValue(drafts.visibilityBio, visibilities.visibilityBio),
courseCertificates,
- visibilityCourseCertificates: chooseFormValue(
- drafts.visibilityCourseCertificates,
- visibilities.visibilityCourseCertificates,
- ),
country: chooseFormValue(drafts.country, account.country),
visibilityCountry: chooseFormValue(drafts.visibilityCountry, visibilities.visibilityCountry),
levelOfEducation: chooseFormValue(drafts.levelOfEducation, account.levelOfEducation),
@@ -318,7 +273,7 @@ export const formValuesSelector = createSelector(
),
name: chooseFormValue(drafts.name, account.name),
visibilityName: chooseFormValue(drafts.visibilityName, visibilities.visibilityName),
- socialLinks, // Social links is calculated in its own selector, since it's complicated.
+ socialLinks,
visibilitySocialLinks: chooseFormValue(
drafts.visibilitySocialLinks,
visibilities.visibilitySocialLinks,
@@ -335,6 +290,7 @@ export const profilePageSelector = createSelector(
isLoadingProfileSelector,
draftSocialLinksByPlatformSelector,
accountErrorsSelector,
+ isAuthenticatedUserProfileSelector,
(
account,
formValues,
@@ -344,47 +300,39 @@ export const profilePageSelector = createSelector(
isLoadingProfile,
draftSocialLinksByPlatform,
errors,
+ isAuthenticatedUserProfile,
) => ({
- // Account data we need
username: account.username,
profileImage,
requiresParentalConsent: account.requiresParentalConsent,
dateJoined: account.dateJoined,
yearOfBirth: account.yearOfBirth,
- // Bio form data
bio: formValues.bio,
visibilityBio: formValues.visibilityBio,
- // Certificates form data
courseCertificates: formValues.courseCertificates,
- visibilityCourseCertificates: formValues.visibilityCourseCertificates,
- // Country form data
country: formValues.country,
visibilityCountry: formValues.visibilityCountry,
- // Education form data
levelOfEducation: formValues.levelOfEducation,
visibilityLevelOfEducation: formValues.visibilityLevelOfEducation,
- // Language proficiency form data
languageProficiencies: formValues.languageProficiencies,
visibilityLanguageProficiencies: formValues.visibilityLanguageProficiencies,
- // Name form data
name: formValues.name,
visibilityName: formValues.visibilityName,
- // Social links form data
socialLinks: formValues.socialLinks,
visibilitySocialLinks: formValues.visibilitySocialLinks,
draftSocialLinksByPlatform,
- // Other data we need
saveState,
savePhotoState,
isLoadingProfile,
photoUploadError: errors.photo || null,
+ isAuthenticatedUserProfile,
}),
);
diff --git a/src/profile/data/services.js b/src/profile/data/services.js
index 17f15a4..e6104df 100644
--- a/src/profile/data/services.js
+++ b/src/profile/data/services.js
@@ -7,7 +7,19 @@ import { FIELD_LABELS } from './constants';
ensureConfig(['LMS_BASE_URL'], 'Profile API service');
function processAccountData(data) {
- return camelCaseObject(data);
+ const processedData = camelCaseObject(data);
+ return {
+ ...processedData,
+ socialLinks: Array.isArray(processedData.socialLinks) ? processedData.socialLinks : [],
+ languageProficiencies: Array.isArray(processedData.languageProficiencies)
+ ? processedData.languageProficiencies : [],
+ name: processedData.name || null,
+ bio: processedData.bio || null,
+ country: processedData.country || null,
+ levelOfEducation: processedData.levelOfEducation || null,
+ profileImage: processedData.profileImage || {},
+ yearOfBirth: processedData.yearOfBirth || null,
+ };
}
function processAndThrowError(error, errorDataProcessor) {
@@ -20,15 +32,12 @@ function processAndThrowError(error, errorDataProcessor) {
}
}
-// GET ACCOUNT
export async function getAccount(username) {
const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`);
- // Process response data
return processAccountData(data);
}
-// PATCH PROFILE
export async function patchProfile(username, params) {
const processedParams = snakeCaseObject(params);
@@ -42,12 +51,9 @@ export async function patchProfile(username, params) {
processAndThrowError(error, processAccountData);
});
- // Process response data
return processAccountData(data);
}
-// POST PROFILE PHOTO
-
export async function postProfilePhoto(username, formData) {
// eslint-disable-next-line no-unused-vars
const { data } = await getHttpClient().post(
@@ -71,8 +77,6 @@ export async function postProfilePhoto(username, formData) {
return updatedData.profileImage;
}
-// DELETE PROFILE PHOTO
-
export async function deleteProfilePhoto(username) {
// eslint-disable-next-line no-unused-vars
const { data } = await getHttpClient().delete(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`);
@@ -86,14 +90,12 @@ export async function deleteProfilePhoto(username) {
return updatedData.profileImage;
}
-// GET PREFERENCES
export async function getPreferences(username) {
const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`);
return camelCaseObject(data);
}
-// PATCH PREFERENCES
export async function patchPreferences(username, params) {
let processedParams = snakeCaseObject(params);
processedParams = convertKeyNames(processedParams, {
@@ -115,8 +117,6 @@ export async function patchPreferences(username, params) {
return params; // TODO: Once the server returns the updated preferences object, return that.
}
-// GET COURSE CERTIFICATES
-
function transformCertificateData(data) {
const transformedData = [];
data.forEach((cert) => {
diff --git a/src/profile/forms/Bio.jsx b/src/profile/forms/Bio.jsx
index e847c97..797bbb6 100644
--- a/src/profile/forms/Bio.jsx
+++ b/src/profile/forms/Bio.jsx
@@ -1,149 +1,140 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Form } from '@openedx/paragon';
+import classNames from 'classnames';
import messages from './Bio.messages';
-// Components
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
-// Selectors
import { editableFormSelector } from '../data/selectors';
+import {
+ useCloseOpenHandler,
+ useHandleChange,
+ useHandleSubmit,
+ useIsOnMobileScreen,
+ useIsVisibilityEnabled,
+} from '../data/hooks';
-class Bio extends React.Component {
- constructor(props) {
- super(props);
+const Bio = ({
+ formId,
+ bio,
+ visibilityBio,
+ editMode,
+ saveState,
+ error,
+ changeHandler,
+ submitHandler,
+ closeHandler,
+ openHandler,
+}) => {
+ const isMobileView = useIsOnMobileScreen();
+ const isVisibilityEnabled = useIsVisibilityEnabled();
+ const intl = useIntl();
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleClose = this.handleClose.bind(this);
- this.handleOpen = this.handleOpen.bind(this);
- }
+ const handleChange = useHandleChange(changeHandler);
+ const handleSubmit = useHandleSubmit(submitHandler, formId);
+ const handleOpen = useCloseOpenHandler(openHandler, formId);
+ const handleClose = useCloseOpenHandler(closeHandler, formId);
- handleChange(e) {
- const { name, value } = e.target;
- this.props.changeHandler(name, value);
- }
-
- handleSubmit(e) {
- e.preventDefault();
- this.props.submitHandler(this.props.formId);
- }
-
- handleClose() {
- this.props.closeHandler(this.props.formId);
- }
-
- handleOpen() {
- this.props.openHandler(this.props.formId);
- }
-
- render() {
- const {
- formId, bio, visibilityBio, editMode, saveState, error, intl,
- } = this.props;
-
- return (
-
-
-
-
- {intl.formatMessage(messages['profile.bio.about.me'])}
-
-
- {error !== null && (
-
- {error}
-
- )}
-
-
+
+
+
+ {intl.formatMessage(messages['profile.bio.about.me'])}
+
+
-
-
- ),
- editable: (
- <>
-
+ {error}
+
+ )}
+
+
- {bio}
- >
- ),
- empty: (
- <>
-
-
-
-
- >
- ),
- static: (
- <>
-
- {bio}
- >
- ),
- }}
- />
- );
- }
-}
+
+
+ ),
+ editable: (
+ <>
+
+ {intl.formatMessage(messages['profile.bio.about.me'])}
+
+
+ >
+ ),
+ empty: (
+ <>
+
+ {intl.formatMessage(messages['profile.bio.about.me'])}
+
+
+
+
+ >
+ ),
+ static: (
+ <>
+
+ {intl.formatMessage(messages['profile.bio.about.me'])}
+
+
+ >
+ ),
+ }}
+ />
+ );
+};
Bio.propTypes = {
- // It'd be nice to just set this as a defaultProps...
- // except the class that comes out on the other side of react-redux's
- // connect() method won't have it anymore. Static properties won't survive
- // through the higher order function.
formId: PropTypes.string.isRequired,
-
- // From Selector
bio: PropTypes.string,
visibilityBio: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
-
- // Actions
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
-
- // i18n
- intl: intlShape.isRequired,
};
Bio.defaultProps = {
@@ -157,4 +148,4 @@ Bio.defaultProps = {
export default connect(
editableFormSelector,
{},
-)(injectIntl(Bio));
+)(Bio);
diff --git a/src/profile/forms/Bio.messages.jsx b/src/profile/forms/Bio.messages.jsx
index 7808710..0860acd 100644
--- a/src/profile/forms/Bio.messages.jsx
+++ b/src/profile/forms/Bio.messages.jsx
@@ -3,7 +3,7 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.bio.about.me': {
id: 'profile.bio.about.me',
- defaultMessage: 'About Me',
+ defaultMessage: 'Bio',
description: 'A section of a user profile',
},
});
diff --git a/src/profile/forms/Certificates.jsx b/src/profile/forms/Certificates.jsx
deleted file mode 100644
index d2ce86e..0000000
--- a/src/profile/forms/Certificates.jsx
+++ /dev/null
@@ -1,231 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import {
- FormattedDate, FormattedMessage, injectIntl, intlShape,
-} from '@edx/frontend-platform/i18n';
-import { Hyperlink } from '@openedx/paragon';
-import { connect } from 'react-redux';
-import get from 'lodash.get';
-
-import messages from './Certificates.messages';
-
-// Components
-import FormControls from './elements/FormControls';
-import EditableItemHeader from './elements/EditableItemHeader';
-import SwitchContent from './elements/SwitchContent';
-
-// Assets
-import professionalCertificateSVG from '../assets/professional-certificate.svg';
-import verifiedCertificateSVG from '../assets/verified-certificate.svg';
-
-// Selectors
-import { certificatesSelector } from '../data/selectors';
-
-class Certificates extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleClose = this.handleClose.bind(this);
- this.handleOpen = this.handleOpen.bind(this);
- }
-
- handleChange(e) {
- const { name, value } = e.target;
- this.props.changeHandler(name, value);
- }
-
- handleSubmit(e) {
- e.preventDefault();
- this.props.submitHandler(this.props.formId);
- }
-
- handleClose() {
- this.props.closeHandler(this.props.formId);
- }
-
- handleOpen() {
- this.props.openHandler(this.props.formId);
- }
-
- renderCertificate({
- certificateType, courseDisplayName, courseOrganization, modifiedDate, downloadUrl, courseId,
- }) {
- const { intl } = this.props;
- const certificateIllustration = (() => {
- switch (certificateType) {
- case 'professional':
- case 'no-id-professional':
- return professionalCertificateSVG;
- case 'verified':
- return verifiedCertificateSVG;
- case 'honor':
- case 'audit':
- default:
- return null;
- }
- })();
-
- return (
-
-
-
-
-
-
- {intl.formatMessage(get(
- messages,
- `profile.certificates.types.${certificateType}`,
- messages['profile.certificates.types.unknown'],
- ))}
-
-
{courseDisplayName}
-
-
-
-
-
{courseOrganization}
-
-
- ,
- }}
- />
-
-
-
- {intl.formatMessage(messages['profile.certificates.view.certificate'])}
-
-
-
-
-
- );
- }
-
- renderCertificates() {
- if (this.props.certificates === null || this.props.certificates.length === 0) {
- return (
-
- );
- }
-
- return (
-
{this.props.certificates.map(certificate => this.renderCertificate(certificate))}
- );
- }
-
- render() {
- const {
- visibilityCourseCertificates, editMode, saveState, intl,
- } = this.props;
-
- return (
-
-
-
-
- {this.renderCertificates()}
-
-
- ),
- editable: (
- <>
-
- {this.renderCertificates()}
- >
- ),
- empty: (
- <>
-
- {this.renderCertificates()}
- >
- ),
- static: (
- <>
-
- {this.renderCertificates()}
- >
- ),
- }}
- />
- );
- }
-}
-
-Certificates.propTypes = {
- // It'd be nice to just set this as a defaultProps...
- // except the class that comes out on the other side of react-redux's
- // connect() method won't have it anymore. Static properties won't survive
- // through the higher order function.
- formId: PropTypes.string.isRequired,
-
- // From Selector
- certificates: PropTypes.arrayOf(PropTypes.shape({
- title: PropTypes.string,
- })),
- visibilityCourseCertificates: PropTypes.oneOf(['private', 'all_users']),
- editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
- saveState: PropTypes.string,
-
- // Actions
- changeHandler: PropTypes.func.isRequired,
- submitHandler: PropTypes.func.isRequired,
- closeHandler: PropTypes.func.isRequired,
- openHandler: PropTypes.func.isRequired,
-
- // i18n
- intl: intlShape.isRequired,
-};
-
-Certificates.defaultProps = {
- editMode: 'static',
- saveState: null,
- visibilityCourseCertificates: 'private',
- certificates: null,
-};
-
-export default connect(
- certificatesSelector,
- {},
-)(injectIntl(Certificates));
diff --git a/src/profile/forms/Certificates.messages.jsx b/src/profile/forms/Certificates.messages.jsx
deleted file mode 100644
index 17d12b8..0000000
--- a/src/profile/forms/Certificates.messages.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { defineMessages } from '@edx/frontend-platform/i18n';
-
-const messages = defineMessages({
- 'profile.certificates.my.certificates': {
- id: 'profile.certificates.my.certificates',
- defaultMessage: 'My Certificates',
- description: 'A section of a user profile',
- },
- 'profile.certificates.view.certificate': {
- id: 'profile.certificates.view.certificate',
- defaultMessage: 'View Certificate',
- description: 'A call to action to view a certificate',
- },
- 'profile.certificates.types.verified': {
- id: 'profile.certificates.types.verified',
- defaultMessage: 'Verified Certificate',
- description: 'A type of certificate a user may have earned',
- },
- 'profile.certificates.types.professional': {
- id: 'profile.certificates.types.professional',
- defaultMessage: 'Professional Certificate',
- description: 'A type of certificate a user may have earned',
- },
- 'profile.certificates.types.unknown': {
- id: 'profile.certificates.types.unknown',
- defaultMessage: 'Certificate',
- description: 'The string to display when a certificate is of an unknown type',
- },
-});
-
-export default messages;
diff --git a/src/profile/forms/Country.jsx b/src/profile/forms/Country.jsx
index bfe5ad3..86cd039 100644
--- a/src/profile/forms/Country.jsx
+++ b/src/profile/forms/Country.jsx
@@ -1,160 +1,137 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { useIntl } from '@edx/frontend-platform/i18n';
import { Form } from '@openedx/paragon';
import messages from './Country.messages';
-// Components
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
-// Selectors
import { countrySelector } from '../data/selectors';
+import {
+ useCloseOpenHandler,
+ useHandleChange,
+ useHandleSubmit,
+ useIsVisibilityEnabled,
+} from '../data/hooks';
-class Country extends React.Component {
- constructor(props) {
- super(props);
+const Country = ({
+ formId,
+ country,
+ visibilityCountry,
+ editMode,
+ saveState,
+ error,
+ translatedCountries,
+ countriesCodesList,
+ countryMessages,
+ changeHandler,
+ submitHandler,
+ closeHandler,
+ openHandler,
+}) => {
+ const isVisibilityEnabled = useIsVisibilityEnabled();
+ const intl = useIntl();
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleClose = this.handleClose.bind(this);
- this.handleOpen = this.handleOpen.bind(this);
- this.isDisabledCountry = this.isDisabledCountry.bind(this);
- }
+ const handleChange = useHandleChange(changeHandler);
+ const handleSubmit = useHandleSubmit(submitHandler, formId);
+ const handleOpen = useCloseOpenHandler(openHandler, formId);
+ const handleClose = useCloseOpenHandler(closeHandler, formId);
- handleChange(e) {
- const {
- name,
- value,
- } = e.target;
- this.props.changeHandler(name, value);
- }
+ const isDisabledCountry = (countryCode) => countriesCodesList.length > 0
+ && !countriesCodesList.find(code => code === countryCode);
- handleSubmit(e) {
- e.preventDefault();
- this.props.submitHandler(this.props.formId);
- }
-
- handleClose() {
- this.props.closeHandler(this.props.formId);
- }
-
- handleOpen() {
- this.props.openHandler(this.props.formId);
- }
-
- isDisabledCountry = (country) => {
- const { countriesCodesList } = this.props;
-
- return countriesCodesList.length > 0 && !countriesCodesList.find(code => code === country);
- };
-
- render() {
- const {
- formId,
- country,
- visibilityCountry,
- editMode,
- saveState,
- error,
- intl,
- translatedCountries,
- countryMessages,
- } = this.props;
-
- return (
-
-
-
+
+
+
+ {intl.formatMessage(messages['profile.country.label'])}
+
+
-
- {intl.formatMessage(messages['profile.country.label'])}
-
-
-
- {translatedCountries.map(({ code, name }) => (
- {name}
- ))}
-
- {error !== null && (
-
- {error}
-
- )}
-
-
-
-
- ),
- editable: (
- <>
-
+ {translatedCountries.map(({ code, name }) => (
+
+ {name}
+
+ ))}
+
+ {error !== null && (
+
+ {error}
+
+ )}
+
+
- {countryMessages[country]}
- >
- ),
- empty: (
- <>
-
-
- {intl.formatMessage(messages['profile.country.empty'])}
-
- >
- ),
- static: (
- <>
-
- {countryMessages[country]}
- >
- ),
- }}
- />
- );
- }
-}
+
+
+ ),
+ editable: (
+ <>
+
+ {intl.formatMessage(messages['profile.country.label'])}
+
+
+ >
+ ),
+ empty: (
+ <>
+
+ {intl.formatMessage(messages['profile.country.label'])}
+
+
+ {intl.formatMessage(messages['profile.country.empty'])}
+
+ >
+ ),
+ static: (
+ <>
+
+ {intl.formatMessage(messages['profile.country.label'])}
+
+
+ >
+ ),
+ }}
+ />
+ );
+};
Country.propTypes = {
- // It'd be nice to just set this as a defaultProps...
- // except the class that comes out on the other side of react-redux's
- // connect() method won't have it anymore. Static properties won't survive
- // through the higher order function.
formId: PropTypes.string.isRequired,
-
- // From Selector
country: PropTypes.string,
visibilityCountry: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
@@ -166,15 +143,10 @@ Country.propTypes = {
})).isRequired,
countriesCodesList: PropTypes.arrayOf(PropTypes.string).isRequired,
countryMessages: PropTypes.objectOf(PropTypes.string).isRequired,
-
- // Actions
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
-
- // i18n
- intl: intlShape.isRequired,
};
Country.defaultProps = {
@@ -188,4 +160,4 @@ Country.defaultProps = {
export default connect(
countrySelector,
{},
-)(injectIntl(Country));
+)(Country);
diff --git a/src/profile/forms/Country.messages.jsx b/src/profile/forms/Country.messages.jsx
index 99875cb..137bff6 100644
--- a/src/profile/forms/Country.messages.jsx
+++ b/src/profile/forms/Country.messages.jsx
@@ -3,12 +3,12 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.country.label': {
id: 'profile.country.label',
- defaultMessage: 'Location',
+ defaultMessage: 'Country',
description: 'The label for a country in a user profile.',
},
'profile.country.empty': {
id: 'profile.country.empty',
- defaultMessage: 'Add location',
+ defaultMessage: 'Add country',
description: 'The affordance to add country location to a user’s profile.',
},
});
diff --git a/src/profile/forms/Education.jsx b/src/profile/forms/Education.jsx
index 8166c2c..5b877fe 100644
--- a/src/profile/forms/Education.jsx
+++ b/src/profile/forms/Education.jsx
@@ -1,180 +1,160 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import get from 'lodash.get';
import { Form } from '@openedx/paragon';
import messages from './Education.messages';
-// Components
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
-// Constants
import { EDUCATION_LEVELS } from '../data/constants';
-// Selectors
import { editableFormSelector } from '../data/selectors';
+import {
+ useCloseOpenHandler,
+ useHandleChange,
+ useHandleSubmit,
+ useIsVisibilityEnabled,
+} from '../data/hooks';
-class Education extends React.Component {
- constructor(props) {
- super(props);
+const Education = ({
+ formId,
+ levelOfEducation,
+ visibilityLevelOfEducation,
+ editMode,
+ saveState,
+ error,
+ changeHandler,
+ submitHandler,
+ closeHandler,
+ openHandler,
+}) => {
+ const isVisibilityEnabled = useIsVisibilityEnabled();
+ const intl = useIntl();
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleClose = this.handleClose.bind(this);
- this.handleOpen = this.handleOpen.bind(this);
- }
+ const handleChange = useHandleChange(changeHandler);
+ const handleSubmit = useHandleSubmit(submitHandler, formId);
+ const handleOpen = useCloseOpenHandler(openHandler, formId);
+ const handleClose = useCloseOpenHandler(closeHandler, formId);
- handleChange(e) {
- const {
- name,
- value,
- } = e.target;
- this.props.changeHandler(name, value);
- }
-
- handleSubmit(e) {
- e.preventDefault();
- this.props.submitHandler(this.props.formId);
- }
-
- handleClose() {
- this.props.closeHandler(this.props.formId);
- }
-
- handleOpen() {
- this.props.openHandler(this.props.formId);
- }
-
- render() {
- const {
- formId, levelOfEducation, visibilityLevelOfEducation, editMode, saveState, error, intl,
- } = this.props;
-
- return (
-
-
-
+
+
+
+ {intl.formatMessage(messages['profile.education.education'])}
+
+
-
- {intl.formatMessage(messages['profile.education.education'])}
-
-
-
- {EDUCATION_LEVELS.map(level => (
-
- {intl.formatMessage(get(
- messages,
- `profile.education.levels.${level}`,
- messages['profile.education.levels.o'],
- ))}
-
- ))}
-
- {error !== null && (
-
- {error}
-
- )}
-
-
-
-
- ),
- editable: (
- <>
-
+ {EDUCATION_LEVELS.map(level => (
+
+ {intl.formatMessage(get(
+ messages,
+ `profile.education.levels.${level}`,
+ messages['profile.education.levels.o'],
+ ))}
+
+ ))}
+
+ {error !== null && (
+
+ {error}
+
+ )}
+
+
-
- {intl.formatMessage(get(
- messages,
- `profile.education.levels.${levelOfEducation}`,
- messages['profile.education.levels.o'],
- ))}
-
- >
- ),
- empty: (
- <>
-
-
-
-
- >
- ),
- static: (
- <>
-
-
- {intl.formatMessage(get(
- messages,
- `profile.education.levels.${levelOfEducation}`,
- messages['profile.education.levels.o'],
- ))}
-
- >
- ),
- }}
- />
- );
- }
-}
+
+
+ ),
+ editable: (
+ <>
+
+ {intl.formatMessage(messages['profile.education.education'])}
+
+
+ >
+ ),
+ empty: (
+ <>
+
+ {intl.formatMessage(messages['profile.education.education'])}
+
+
+
+
+ >
+ ),
+ static: (
+ <>
+
+ {intl.formatMessage(messages['profile.education.education'])}
+
+
+ >
+ ),
+ }}
+ />
+ );
+};
Education.propTypes = {
- // It'd be nice to just set this as a defaultProps...
- // except the class that comes out on the other side of react-redux's
- // connect() method won't have it anymore. Static properties won't survive
- // through the higher order function.
formId: PropTypes.string.isRequired,
-
- // From Selector
levelOfEducation: PropTypes.string,
visibilityLevelOfEducation: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
-
- // Actions
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
-
- // i18n
- intl: intlShape.isRequired,
};
Education.defaultProps = {
@@ -188,4 +168,4 @@ Education.defaultProps = {
export default connect(
editableFormSelector,
{},
-)(injectIntl(Education));
+)(Education);
diff --git a/src/profile/forms/LearningGoal.jsx b/src/profile/forms/LearningGoal.jsx
deleted file mode 100644
index 236377f..0000000
--- a/src/profile/forms/LearningGoal.jsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import get from 'lodash.get';
-
-// Mock Data
-import mockData from '../data/mock_data';
-
-import messages from './LearningGoal.messages';
-
-// Components
-import EditableItemHeader from './elements/EditableItemHeader';
-import SwitchContent from './elements/SwitchContent';
-
-// Selectors
-import { editableFormSelector } from '../data/selectors';
-
-const LearningGoal = (props) => {
- let { learningGoal, editMode, visibilityLearningGoal } = props;
- const { intl } = props;
-
- if (!learningGoal) {
- learningGoal = mockData.learningGoal;
- }
-
- if (!editMode || editMode === 'empty') { // editMode defaults to 'empty', not sure why yet
- editMode = mockData.editMode;
- }
-
- if (!visibilityLearningGoal) {
- visibilityLearningGoal = mockData.visibilityLearningGoal;
- }
-
- return (
-
-
-
- {intl.formatMessage(get(
- messages,
- `profile.learningGoal.options.${learningGoal}`,
- messages['profile.learningGoal.options.something_else'],
- ))}
-
- >
- ),
- static: (
- <>
-
-
- {intl.formatMessage(get(
- messages,
- `profile.learningGoal.options.${learningGoal}`,
- messages['profile.learningGoal.options.something_else'],
- ))}
-
- >
- ),
- }}
- />
- );
-};
-
-LearningGoal.propTypes = {
- // From Selector
- learningGoal: PropTypes.oneOf(['advance_career', 'start_career', 'learn_something_new', 'something_else']),
- visibilityLearningGoal: PropTypes.oneOf(['private', 'all_users']),
- editMode: PropTypes.oneOf(['editable', 'static']),
-
- // i18n
- intl: intlShape.isRequired,
-};
-
-LearningGoal.defaultProps = {
- editMode: 'static',
- learningGoal: null,
- visibilityLearningGoal: 'private',
-};
-
-export default connect(
- editableFormSelector,
- {},
-)(injectIntl(LearningGoal));
diff --git a/src/profile/forms/LearningGoal.messages.jsx b/src/profile/forms/LearningGoal.messages.jsx
deleted file mode 100644
index 181fbb3..0000000
--- a/src/profile/forms/LearningGoal.messages.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { defineMessages } from '@edx/frontend-platform/i18n';
-
-const messages = defineMessages({
- 'profile.learningGoal.learningGoal': {
- id: 'profile.learningGoal.learningGoal',
- defaultMessage: 'Learning Goal',
- description: 'A section of a user profile that displays their current learning goal.',
- },
- 'profile.learningGoal.options.start_career': {
- id: 'profile.learningGoal.options.start_career',
- defaultMessage: 'I want to start my career',
- description: 'Selected by user if their goal is to start their career.',
- },
- 'profile.learningGoal.options.advance_career': {
- id: 'profile.learningGoal.options.advance_career',
- defaultMessage: 'I want to advance my career',
- description: 'Selected by user if their goal is to advance their career.',
- },
- 'profile.learningGoal.options.learn_something_new': {
- id: 'profile.learningGoal.options.learn_something_new',
- defaultMessage: 'I want to learn something new',
- description: 'Selected by user if their goal is to learn something new.',
- },
- 'profile.learningGoal.options.something_else': {
- id: 'profile.learningGoal.options.something_else',
- defaultMessage: 'Something else',
- description: 'Selected by user if their goal is not described by the other choices.',
- },
-});
-
-export default messages;
diff --git a/src/profile/forms/LearningGoal.test.jsx b/src/profile/forms/LearningGoal.test.jsx
deleted file mode 100644
index dcb63b1..0000000
--- a/src/profile/forms/LearningGoal.test.jsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { useMemo } from 'react';
-import { Provider } from 'react-redux';
-import { render, screen } from '@testing-library/react';
-import configureMockStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n';
-import { getConfig } from '@edx/frontend-platform';
-import { AppContext } from '@edx/frontend-platform/react';
-import messages from '../../i18n';
-
-import viewOwnProfileMockStore from '../__mocks__/viewOwnProfile.mockStore';
-import savingEditedBioMockStore from '../__mocks__/savingEditedBio.mockStore';
-
-import LearningGoal from './LearningGoal';
-
-const mockStore = configureMockStore([thunk]);
-
-// props to be passed down to LearningGoal component
-const requiredLearningGoalProps = {
- formId: 'learningGoal',
- learningGoal: 'advance_career',
- drafts: {},
- visibilityLearningGoal: 'private',
- editMode: 'static',
- saveState: null,
- error: null,
- openHandler: jest.fn(),
-};
-
-configureI18n({
- loggingService: { logError: jest.fn() },
- config: {
- ENVIRONMENT: 'production',
- LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
- },
- messages,
-});
-
-const LearningGoalWrapper = (props) => {
- const contextValue = useMemo(() => ({
- authenticatedUser: { userId: null, username: null, administrator: false },
- config: getConfig(),
- }), []);
- return (
-
-
-
-
-
-
-
- );
-};
-
-LearningGoalWrapper.defaultProps = {
- store: mockStore(viewOwnProfileMockStore),
-};
-
-LearningGoalWrapper.propTypes = {
- store: PropTypes.shape({}),
-};
-
-const LearningGoalWrapperWithStore = ({ store }) => {
- const contextValue = useMemo(() => ({
- authenticatedUser: { userId: null, username: null, administrator: false },
- config: getConfig(),
- }), []);
- return (
-
-
-
-
-
-
-
- );
-};
-
-LearningGoalWrapperWithStore.defaultProps = {
- store: mockStore(savingEditedBioMockStore),
-};
-
-LearningGoalWrapperWithStore.propTypes = {
- store: PropTypes.shape({}),
-};
-
-describe(' ', () => {
- describe('renders the current learning goal', () => {
- it('renders "I want to advance my career"', () => {
- render(
- ,
- );
- expect(screen.getByText('I want to advance my career')).toBeTruthy();
- });
-
- it('renders "Something else"', () => {
- requiredLearningGoalProps.learningGoal = 'something_else';
-
- render(
- ,
- );
- expect(screen.getByText('Something else')).toBeTruthy();
- });
- });
-});
diff --git a/src/profile/forms/Name.jsx b/src/profile/forms/Name.jsx
index 889a9ea..f3e2993 100644
--- a/src/profile/forms/Name.jsx
+++ b/src/profile/forms/Name.jsx
@@ -1,147 +1,182 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { InfoOutline } from '@openedx/paragon/icons';
+import { Hyperlink, OverlayTrigger, Tooltip } from '@openedx/paragon';
import messages from './Name.messages';
-// Components
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
-// Selectors
import { editableFormSelector } from '../data/selectors';
+import {
+ useCloseOpenHandler,
+ useHandleChange,
+ useHandleSubmit,
+ useIsVisibilityEnabled,
+} from '../data/hooks';
-class Name extends React.Component {
- constructor(props) {
- super(props);
+const Name = ({
+ formId,
+ name,
+ visibilityName,
+ editMode,
+ saveState,
+ changeHandler,
+ submitHandler,
+ closeHandler,
+ openHandler,
+ accountSettingsUrl,
+}) => {
+ const isVisibilityEnabled = useIsVisibilityEnabled();
+ const intl = useIntl();
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleClose = this.handleClose.bind(this);
- this.handleOpen = this.handleOpen.bind(this);
- }
+ const handleChange = useHandleChange(changeHandler);
+ const handleSubmit = useHandleSubmit(submitHandler, formId);
+ const handleOpen = useCloseOpenHandler(openHandler, formId);
+ const handleClose = useCloseOpenHandler(closeHandler, formId);
- handleChange(e) {
- const {
- name,
- value,
- } = e.target;
- this.props.changeHandler(name, value);
- }
-
- handleSubmit(e) {
- e.preventDefault();
- this.props.submitHandler(this.props.formId);
- }
-
- handleClose() {
- this.props.closeHandler(this.props.formId);
- }
-
- handleOpen() {
- this.props.openHandler(this.props.formId);
- }
-
- render() {
- const {
- formId, name, visibilityName, editMode, saveState, intl,
- } = this.props;
-
- return (
-
-
-
-
- {/*
- This isn't a mistake - the name field should not be editable. But if it were,
- you'd find the original code got deleted in the commit which added this comment.
- -djoy
- TODO: Relatedly, the plumbing for editing the name field is still in place.
- Once we're super sure we don't want it back, you could delete the name props and
- such to fully get rid of it.
- */}
-
{name}
-
- {intl.formatMessage(messages['profile.name.details'])}
-
+ return (
+
+
+
+
+
+ {intl.formatMessage(messages['profile.name.full.name'])}
+
+
+
+ {intl.formatMessage(messages['profile.name.tooltip'])}
+
+
+ )}
+ >
+
+
-
-
-
- ),
- editable: (
- <>
-
+
+
+ {intl.formatMessage(messages['profile.name.redirect'])}
+
+
+
+
- {name}
-
- {intl.formatMessage(messages['profile.name.details'])}
-
- >
- ),
- empty: (
- <>
-
-
- {intl.formatMessage(messages['profile.name.empty'])}
-
-
- {intl.formatMessage(messages['profile.name.details'])}
-
- >
- ),
- static: (
- <>
-
- {name}
- >
- ),
- }}
- />
- );
- }
-}
+
+
+ ),
+ editable: (
+ <>
+
+
+ {intl.formatMessage(messages['profile.name.full.name'])}
+
+
+
+ {intl.formatMessage(messages['profile.name.tooltip'])}
+
+
+ )}
+ >
+
+
+
+
+ >
+ ),
+ empty: (
+ <>
+
+
+ {intl.formatMessage(messages['profile.name.full.name'])}
+
+
+
+ {intl.formatMessage(messages['profile.name.tooltip'])}
+
+
+ )}
+ >
+
+
+
+
+ {intl.formatMessage(messages['profile.name.empty'])}
+
+ >
+ ),
+ static: (
+ <>
+
+
+ {intl.formatMessage(messages['profile.name.full.name'])}
+
+
+
+ {intl.formatMessage(messages['profile.name.tooltip'])}
+
+
+ )}
+ >
+
+
+
+
+ >
+ ),
+ }}
+ />
+ );
+};
Name.propTypes = {
- // It'd be nice to just set this as a defaultProps...
- // except the class that comes out on the other side of react-redux's
- // connect() method won't have it anymore. Static properties won't survive
- // through the higher order function.
formId: PropTypes.string.isRequired,
-
- // From Selector
name: PropTypes.string,
visibilityName: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
-
- // Actions
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
-
- // i18n
- intl: intlShape.isRequired,
+ accountSettingsUrl: PropTypes.string.isRequired,
};
Name.defaultProps = {
@@ -154,4 +189,4 @@ Name.defaultProps = {
export default connect(
editableFormSelector,
{},
-)(injectIntl(Name));
+)(Name);
diff --git a/src/profile/forms/Name.messages.jsx b/src/profile/forms/Name.messages.jsx
index d6a42fc..e9e9317 100644
--- a/src/profile/forms/Name.messages.jsx
+++ b/src/profile/forms/Name.messages.jsx
@@ -3,19 +3,24 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.name.full.name': {
id: 'profile.name.full.name',
- defaultMessage: 'Full Name',
+ defaultMessage: 'Full name',
description: 'A section of a user profile',
},
- 'profile.name.details': {
- id: 'profile.name.details',
- defaultMessage: 'This is the name that appears in your account and on your certificates.',
- description: 'Describes the area for a user to update their name.',
- },
'profile.name.empty': {
id: 'profile.name.empty',
- defaultMessage: 'Add name',
+ defaultMessage: 'Add full name',
description: 'The affordance to add a name to a user’s profile.',
},
+ 'profile.name.tooltip': {
+ id: 'profile.name.tooltip',
+ defaultMessage: 'The name that is used for ID verification and that appears on your certificates',
+ description: 'Tooltip for the full name field.',
+ },
+ 'profile.name.redirect': {
+ id: 'profile.name.redirect',
+ defaultMessage: 'Edit full name from the Accounts page',
+ description: 'Redirect message for editing the name from the Accounts page.',
+ },
});
export default messages;
diff --git a/src/profile/forms/PreferredLanguage.jsx b/src/profile/forms/PreferredLanguage.jsx
index 1ce3081..54e0e17 100644
--- a/src/profile/forms/PreferredLanguage.jsx
+++ b/src/profile/forms/PreferredLanguage.jsx
@@ -1,166 +1,140 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { useIntl } from '@edx/frontend-platform/i18n';
import { Form } from '@openedx/paragon';
import messages from './PreferredLanguage.messages';
-// Components
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
-// Selectors
import { preferredLanguageSelector } from '../data/selectors';
+import {
+ useCloseOpenHandler,
+ useHandleSubmit,
+ useIsVisibilityEnabled,
+} from '../data/hooks';
-class PreferredLanguage extends React.Component {
- constructor(props) {
- super(props);
+const PreferredLanguage = ({
+ formId,
+ languageProficiencies,
+ visibilityLanguageProficiencies,
+ editMode,
+ saveState,
+ error,
+ sortedLanguages,
+ languageMessages,
+ changeHandler,
+ submitHandler,
+ closeHandler,
+ openHandler,
+}) => {
+ const isVisibilityEnabled = useIsVisibilityEnabled();
+ const intl = useIntl();
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleClose = this.handleClose.bind(this);
- this.handleOpen = this.handleOpen.bind(this);
- }
-
- handleChange(e) {
- const { name, value } = e.target;
- // Restructure the data.
- // We deconstruct our value prop in render() so this
- // changes our data's shape back to match what came in
- if (name === this.props.formId) {
- if (value !== '') {
- this.props.changeHandler(name, [{ code: value }]);
- } else {
- this.props.changeHandler(name, []);
- }
- } else {
- this.props.changeHandler(name, value);
+ const handleChange = ({ target: { name, value } }) => {
+ let newValue = value;
+ if (name === formId) {
+ newValue = value ? [{ code: value }] : [];
}
- }
+ changeHandler(name, newValue);
+ };
- handleSubmit(e) {
- e.preventDefault();
- this.props.submitHandler(this.props.formId);
- }
+ const handleSubmit = useHandleSubmit(submitHandler, formId);
+ const handleOpen = useCloseOpenHandler(openHandler, formId);
+ const handleClose = useCloseOpenHandler(closeHandler, formId);
- handleClose() {
- this.props.closeHandler(this.props.formId);
- }
+ const value = languageProficiencies.length ? languageProficiencies[0].code : '';
- handleOpen() {
- this.props.openHandler(this.props.formId);
- }
-
- render() {
- const {
- formId,
- languageProficiencies,
- visibilityLanguageProficiencies,
- editMode,
- saveState,
- error,
- intl,
- sortedLanguages,
- languageMessages,
- } = this.props;
-
- const value = languageProficiencies.length ? languageProficiencies[0].code : '';
-
- return (
-
-
-
+
+
+
+ {intl.formatMessage(messages['profile.preferredlanguage.label'])}
+
+
-
- {intl.formatMessage(messages['profile.preferredlanguage.label'])}
-
-
-
- {sortedLanguages.map(({ code, name }) => (
- {name}
- ))}
-
- {error !== null && (
-
- {error}
-
- )}
-
-
-
-
- ),
- editable: (
- <>
-
+ {sortedLanguages.map(({ code, name }) => (
+ {name}
+ ))}
+
+ {error !== null && (
+
+ {error}
+
+ )}
+
+
- {languageMessages[value]}
- >
- ),
- empty: (
- <>
-
-
- {intl.formatMessage(messages['profile.preferredlanguage.empty'])}
-
- >
- ),
- static: (
- <>
-
- {languageMessages[value]}
- >
- ),
- }}
- />
- );
- }
-}
+
+
+ ),
+ editable: (
+ <>
+
+ {intl.formatMessage(messages['profile.preferredlanguage.label'])}
+
+
+ >
+ ),
+ empty: (
+ <>
+
+ {intl.formatMessage(messages['profile.preferredlanguage.label'])}
+
+
+ {intl.formatMessage(messages['profile.preferredlanguage.empty'])}
+
+ >
+ ),
+ static: (
+ <>
+
+ {intl.formatMessage(messages['profile.preferredlanguage.label'])}
+
+
+ >
+ ),
+ }}
+ />
+ );
+};
PreferredLanguage.propTypes = {
- // It'd be nice to just set this as a defaultProps...
- // except the class that comes out on the other side of react-redux's
- // connect() method won't have it anymore. Static properties won't survive
- // through the higher order function.
formId: PropTypes.string.isRequired,
-
- // From Selector
languageProficiencies: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.shape({ code: PropTypes.string })),
- // TODO: ProfilePageSelector should supply null values
- // instead of empty strings when no value exists
PropTypes.oneOf(['']),
]),
visibilityLanguageProficiencies: PropTypes.oneOf(['private', 'all_users']),
@@ -172,15 +146,10 @@ PreferredLanguage.propTypes = {
name: PropTypes.string.isRequired,
})).isRequired,
languageMessages: PropTypes.objectOf(PropTypes.string).isRequired,
-
- // Actions
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
-
- // i18n
- intl: intlShape.isRequired,
};
PreferredLanguage.defaultProps = {
@@ -194,4 +163,4 @@ PreferredLanguage.defaultProps = {
export default connect(
preferredLanguageSelector,
{},
-)(injectIntl(PreferredLanguage));
+)(PreferredLanguage);
diff --git a/src/profile/forms/PreferredLanguage.messages.jsx b/src/profile/forms/PreferredLanguage.messages.jsx
index c91a009..f24ed1b 100644
--- a/src/profile/forms/PreferredLanguage.messages.jsx
+++ b/src/profile/forms/PreferredLanguage.messages.jsx
@@ -8,7 +8,7 @@ const messages = defineMessages({
},
'profile.preferredlanguage.label': {
id: 'profile.preferredlanguage.label',
- defaultMessage: 'Primary Language Spoken',
+ defaultMessage: 'Primary language spoken',
description: 'The label for a user’s primary spoken language.',
},
});
diff --git a/src/profile/forms/ProfileAvatar.jsx b/src/profile/forms/ProfileAvatar.jsx
index 19ce9c7..0b0d968 100644
--- a/src/profile/forms/ProfileAvatar.jsx
+++ b/src/profile/forms/ProfileAvatar.jsx
@@ -1,158 +1,155 @@
-import React from 'react';
+import React, { useRef } from 'react';
import PropTypes from 'prop-types';
-import { Button, Dropdown } from '@openedx/paragon';
-import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import {
+ Dropdown,
+ IconButton,
+ Icon,
+ Tooltip,
+ OverlayTrigger,
+} from '@openedx/paragon';
+import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
+import { PhotoCamera } from '@openedx/paragon/icons';
import { ReactComponent as DefaultAvatar } from '../assets/avatar.svg';
-
import messages from './ProfileAvatar.messages';
-class ProfileAvatar extends React.Component {
- constructor(props) {
- super(props);
+const ProfileAvatar = ({
+ src,
+ isDefault,
+ onSave,
+ onDelete,
+ savePhotoState,
+ isEditable,
+}) => {
+ const intl = useIntl();
+ const fileInput = useRef(null);
+ const form = useRef(null);
- this.fileInput = React.createRef();
- this.form = React.createRef();
+ const onClickUpload = () => {
+ fileInput.current.click();
+ };
- this.onClickUpload = this.onClickUpload.bind(this);
- this.onClickDelete = this.onClickDelete.bind(this);
- this.onChangeInput = this.onChangeInput.bind(this);
- this.onSubmit = this.onSubmit.bind(this);
- }
+ const onClickDelete = () => {
+ onDelete();
+ };
- onClickUpload() {
- this.fileInput.current.click();
- }
-
- onClickDelete() {
- this.props.onDelete();
- }
-
- onChangeInput() {
- this.onSubmit();
- }
-
- onSubmit(e) {
+ const onSubmit = (e) => {
if (e) {
e.preventDefault();
}
- this.props.onSave(new FormData(this.form.current));
- this.form.current.reset();
- }
+ onSave(new FormData(form.current));
+ form.current.reset();
+ };
- renderPending() {
- return (
-
- );
- }
+ const onChangeInput = () => {
+ onSubmit();
+ };
- renderMenuContent() {
- const { intl } = this.props;
+ const renderPending = () => (
+
+ );
- if (this.props.isDefault) {
- return (
-
-
-
- );
- }
-
- return (
-
-
- {intl.formatMessage(messages['profile.profileavatar.change-button'])}
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- renderMenu() {
- if (!this.props.isEditable) {
+ const renderEditButton = () => {
+ if (!isEditable) {
return null;
}
return (
-
- {this.renderMenuContent()}
+
+
+
+ {!isDefault ? (
+
+ {intl.formatMessage(messages['profile.profileavatar.tooltip.edit'])}
+
+ ) : (
+
+ {intl.formatMessage(messages['profile.profileavatar.tooltip.upload'])}
+
+ )}
+
+ )}
+ >
+
+
+
+
+
+
+ {!isDefault && (
+
+
+
+ )}
+
+
);
- }
+ };
- renderAvatar() {
- const { intl } = this.props;
-
- return this.props.isDefault ? (
+ const renderAvatar = () => (
+ isDefault ? (
) : (
- );
- }
+ )
+ );
- render() {
- return (
-
-
- {this.props.savePhotoState === 'pending' ? this.renderPending() : this.renderMenu() }
- {this.renderAvatar()}
-
-
- {/* The name of this input must be 'file' */}
-
-
+ return (
+
+
+ {savePhotoState === 'pending' && renderPending()}
+ {renderAvatar()}
- );
- }
-}
-
-export default injectIntl(ProfileAvatar);
+ {renderEditButton()}
+
+
+
+
+ );
+};
ProfileAvatar.propTypes = {
src: PropTypes.string,
@@ -161,7 +158,6 @@ ProfileAvatar.propTypes = {
onDelete: PropTypes.func.isRequired,
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
isEditable: PropTypes.bool,
- intl: intlShape.isRequired,
};
ProfileAvatar.defaultProps = {
@@ -170,3 +166,5 @@ ProfileAvatar.defaultProps = {
savePhotoState: null,
isEditable: false,
};
+
+export default ProfileAvatar;
diff --git a/src/profile/forms/ProfileAvatar.messages.jsx b/src/profile/forms/ProfileAvatar.messages.jsx
index 121da2b..5076ad1 100644
--- a/src/profile/forms/ProfileAvatar.messages.jsx
+++ b/src/profile/forms/ProfileAvatar.messages.jsx
@@ -11,6 +11,16 @@ const messages = defineMessages({
defaultMessage: 'Change',
description: 'Change photo button',
},
+ 'profile.profileavatar.tooltip.edit': {
+ id: 'profile.profileavatar.tooltip.edit',
+ defaultMessage: 'Edit photo',
+ description: 'Tooltip for edit photo button',
+ },
+ 'profile.profileavatar.tooltip.upload': {
+ id: 'profile.profileavatar.tooltip.upload',
+ defaultMessage: 'Upload photo',
+ description: 'Tooltip for upload photo button',
+ },
});
export default messages;
diff --git a/src/profile/forms/SocialLinks.jsx b/src/profile/forms/SocialLinks.jsx
index f7d178e..60677fc 100644
--- a/src/profile/forms/SocialLinks.jsx
+++ b/src/profile/forms/SocialLinks.jsx
@@ -1,22 +1,18 @@
-import React from 'react';
+import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Alert } from '@openedx/paragon';
import { connect } from 'react-redux';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTwitter, faFacebook, faLinkedin } from '@fortawesome/free-brands-svg-icons';
-import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
import classNames from 'classnames';
-import messages from './SocialLinks.messages';
-
-// Components
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
-// Selectors
import { editableFormSelector } from '../data/selectors';
+import { useIsVisibilityEnabled } from '../data/hooks';
const platformDisplayInfo = {
facebook: {
@@ -25,7 +21,7 @@ const platformDisplayInfo = {
},
twitter: {
icon: faTwitter,
- name: 'Twitter',
+ name: 'X',
},
linkedin: {
icon: faLinkedin,
@@ -33,283 +29,203 @@ const platformDisplayInfo = {
},
};
-const SocialLink = ({ url, name, platform }) => (
-
-
- {name}
-
-);
-
-SocialLink.propTypes = {
- url: PropTypes.string.isRequired,
- platform: PropTypes.string.isRequired,
- name: PropTypes.string.isRequired,
-};
-
-const EditableListItem = ({
- url, platform, onClickEmptyContent, name,
+const SocialLinks = ({
+ formId,
+ socialLinks,
+ draftSocialLinksByPlatform,
+ visibilitySocialLinks,
+ editMode,
+ saveState,
+ error,
+ changeHandler,
+ submitHandler,
+ closeHandler,
+ openHandler,
}) => {
- const linkDisplay = url ? (
-
- ) : (
-
Add {name}
- );
+ const isVisibilityEnabled = useIsVisibilityEnabled();
+ const [activePlatform, setActivePlatform] = useState(null);
- return
{linkDisplay} ;
-};
-
-EditableListItem.propTypes = {
- url: PropTypes.string,
- platform: PropTypes.string.isRequired,
- name: PropTypes.string.isRequired,
- onClickEmptyContent: PropTypes.func,
-};
-EditableListItem.defaultProps = {
- url: null,
- onClickEmptyContent: null,
-};
-
-const EditingListItem = ({
- platform, name, value, onChange, error,
-}) => (
-
- {name}
-
-
-);
-
-EditingListItem.propTypes = {
- platform: PropTypes.string.isRequired,
- value: PropTypes.string,
- name: PropTypes.string.isRequired,
- onChange: PropTypes.func.isRequired,
- error: PropTypes.string,
-};
-
-EditingListItem.defaultProps = {
- value: null,
- error: null,
-};
-
-const EmptyListItem = ({ onClick, name }) => (
-
-
-
-
-
-);
-
-EmptyListItem.propTypes = {
- name: PropTypes.string.isRequired,
- onClick: PropTypes.func.isRequired,
-};
-
-const StaticListItem = ({ name, url, platform }) => (
-
-
-
-);
-
-StaticListItem.propTypes = {
- name: PropTypes.string.isRequired,
- url: PropTypes.string,
- platform: PropTypes.string.isRequired,
-};
-
-StaticListItem.defaultProps = {
- url: null,
-};
-
-class SocialLinks extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleClose = this.handleClose.bind(this);
- this.handleOpen = this.handleOpen.bind(this);
- }
-
- handleChange(e) {
- const { name, value } = e.target;
-
- // The social links are a bit special. If we're updating them, we need to merge them
- // with any existing social link drafts, essentially sending a fresh copy of the whole
- // data structure back to the reducer. This helps the reducer stay simple and keeps
- // special cases out of it, concentrating them here, where they began.
- if (name !== 'visibilitySocialLinks') {
- this.props.changeHandler(
- 'socialLinks',
- this.mergeWithDrafts({
- platform: name,
- // If it's an empty string, send it as null.
- // The empty string is just for the input. We want nulls.
- socialLink: value,
- }),
- );
- } else {
- this.props.changeHandler(name, value);
- }
- }
-
- handleSubmit(e) {
- e.preventDefault();
- this.props.submitHandler(this.props.formId);
- }
-
- handleClose() {
- this.props.closeHandler(this.props.formId);
- }
-
- handleOpen() {
- this.props.openHandler(this.props.formId);
- }
-
- mergeWithDrafts(newSocialLink) {
+ const mergeWithDrafts = (newSocialLink) => {
const knownPlatforms = ['twitter', 'facebook', 'linkedin'];
const updated = [];
knownPlatforms.forEach((platform) => {
if (newSocialLink.platform === platform) {
updated.push(newSocialLink);
- } else if (this.props.draftSocialLinksByPlatform[platform] !== undefined) {
- updated.push(this.props.draftSocialLinksByPlatform[platform]);
+ } else if (draftSocialLinksByPlatform[platform] !== undefined) {
+ updated.push(draftSocialLinksByPlatform[platform]);
}
});
return updated;
- }
+ };
- render() {
- const {
- socialLinks, visibilitySocialLinks, editMode, saveState, error, intl,
- } = this.props;
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ if (name !== 'visibilitySocialLinks') {
+ changeHandler(
+ 'socialLinks',
+ mergeWithDrafts({
+ platform: name,
+ socialLink: value,
+ }),
+ );
+ } else {
+ changeHandler(name, value);
+ }
+ };
- return (
-
-
-
- {socialLinks.map(({ platform }) => (
-
- ))}
-
- >
- ),
- static: (
- <>
- {
+ e.preventDefault();
+ submitHandler(formId);
+ setActivePlatform(null);
+ };
+
+ const handleClose = () => {
+ closeHandler(formId);
+ setActivePlatform(null);
+ };
+
+ const handleOpen = (platform) => {
+ openHandler(formId);
+ setActivePlatform(platform);
+ };
+
+ const renderPlatformContent = (platform, socialLink, isEditing) => {
+ if (isEditing) {
+ return (
+
+
+ {error !== null && (
+
+ )}
+
+
-
- {socialLinks
- .filter(({ socialLink }) => Boolean(socialLink))
- .map(({ platform, socialLink }) => (
-
- ))}
-
- >
- ),
- editable: (
- <>
-
-
- {socialLinks.map(({ platform, socialLink }) => (
-
- ))}
-
- >
- ),
- editing: (
-
-
-
- {/* TODO: Replace this alert with per-field errors. Needs API update. */}
-
- {error !== null
- ? (
-
- {error}
-
- ) : null}
-
-
- {socialLinks.map(({ platform, socialLink }) => (
-
- ))}
-
-
-
- ),
- }}
- />
+
+
+
+ );
+ }
+ if (socialLink) {
+ return (
+
+ handleOpen(platform)}
+ showVisibility={visibilitySocialLinks !== null && isVisibilityEnabled}
+ visibility={visibilitySocialLinks}
+ />
+
+ );
+ }
+ return (
+
handleOpen(platform)}>
+ Add {platformDisplayInfo[platform].name}
+
);
- }
-}
+ };
+
+ return (
+
+
+ {socialLinks.map(({ platform }) => (
+
+
+ {platformDisplayInfo[platform].name}
+
+
handleOpen(platform)}>
+
+
+
+ ))}
+
+
+ ),
+ static: (
+
+
+ {socialLinks
+ .filter(({ socialLink }) => Boolean(socialLink))
+ .map(({ platform, socialLink }) => (
+
+
+ {platformDisplayInfo[platform].name}
+
+
+
+ ))}
+
+
+ ),
+ editable: (
+
+
+ {socialLinks.map(({ platform, socialLink }) => (
+
+
+ {platformDisplayInfo[platform].name}
+
+ {renderPlatformContent(platform, socialLink, activePlatform === platform)}
+
+ ))}
+
+
+ ),
+ editing: (
+
+
+ {socialLinks.map(({ platform, socialLink }) => (
+
+
+ {platformDisplayInfo[platform].name}
+
+ {renderPlatformContent(platform, socialLink, activePlatform === platform)}
+
+ ))}
+
+
+ ),
+ }}
+ />
+ );
+};
SocialLinks.propTypes = {
- // It'd be nice to just set this as a defaultProps...
- // except the class that comes out on the other side of react-redux's
- // connect() method won't have it anymore. Static properties won't survive
- // through the higher order function.
formId: PropTypes.string.isRequired,
-
- // From Selector
socialLinks: PropTypes.arrayOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
@@ -322,15 +238,10 @@ SocialLinks.propTypes = {
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
-
- // Actions
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
-
- // i18n
- intl: intlShape.isRequired,
};
SocialLinks.defaultProps = {
@@ -344,4 +255,4 @@ SocialLinks.defaultProps = {
export default connect(
editableFormSelector,
{},
-)(injectIntl(SocialLinks));
+)(SocialLinks);
diff --git a/src/profile/forms/SocialLinks.test.jsx b/src/profile/forms/SocialLinks.test.jsx
deleted file mode 100644
index 0392d06..0000000
--- a/src/profile/forms/SocialLinks.test.jsx
+++ /dev/null
@@ -1,165 +0,0 @@
-import { render, fireEvent, screen } from '@testing-library/react';
-import PropTypes from 'prop-types';
-import React, { useMemo } from 'react';
-import { Provider } from 'react-redux';
-import configureMockStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n';
-import { getConfig } from '@edx/frontend-platform';
-import { AppContext } from '@edx/frontend-platform/react';
-
-import SocialLinks from './SocialLinks';
-import * as savingEditedBio from '../__mocks__/savingEditedBio.mockStore';
-import messages from '../../i18n';
-
-const mockStore = configureMockStore([thunk]);
-
-const defaultProps = {
- formId: 'socialLinks',
- socialLinks: [
- {
- platform: 'facebook',
- socialLink: 'https://www.facebook.com/aloha',
- },
- {
- platform: 'twitter',
- socialLink: 'https://www.twitter.com/ALOHA',
- },
- ],
- drafts: {},
- visibilitySocialLinks: 'private',
- editMode: 'static',
- saveState: null,
- error: null,
- changeHandler: jest.fn(),
- submitHandler: jest.fn(),
- closeHandler: jest.fn(),
- openHandler: jest.fn(),
-};
-
-configureI18n({
- loggingService: { logError: jest.fn() },
- config: {
- ENVIRONMENT: 'production',
- LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
- },
- messages,
-});
-
-const SocialLinksWrapper = (props) => {
- const contextValue = useMemo(() => ({
- authenticatedUser: { userId: null, username: null, administrator: false },
- config: getConfig(),
- }), []);
- return (
-
-
-
-
-
-
-
- );
-};
-
-SocialLinksWrapper.defaultProps = {
- store: mockStore(savingEditedBio),
-};
-
-SocialLinksWrapper.propTypes = {
- store: PropTypes.shape({}),
-};
-
-const SocialLinksWrapperWithStore = ({ store }) => {
- const contextValue = useMemo(() => ({
- authenticatedUser: { userId: null, username: null, administrator: false },
- config: getConfig(),
- }), []);
- return (
-
-
-
-
-
-
-
- );
-};
-
-SocialLinksWrapperWithStore.defaultProps = {
- store: mockStore(savingEditedBio),
-};
-
-SocialLinksWrapperWithStore.propTypes = {
- store: PropTypes.shape({}),
-};
-
-describe(' ', () => {
- ['certificates', 'bio', 'goals', 'socialLinks'].forEach(editMode => (
- it(`calls social links with edit mode ${editMode}`, () => {
- const component = ;
- const { container: tree } = render(component);
- expect(tree).toMatchSnapshot();
- })
- ));
-
- it('calls social links with editing', () => {
- const changeHandler = jest.fn();
- const submitHandler = jest.fn();
- const closeHandler = jest.fn();
- const { container } = render(
- ,
- );
-
- const { platform } = defaultProps.socialLinks[0];
- const inputField = container.querySelector(`#social-${platform}`);
- fireEvent.change(inputField, { target: { value: 'test', name: platform } });
- expect(changeHandler).toHaveBeenCalledTimes(1);
-
- const selectElement = container.querySelector('#visibilitySocialLinks');
- expect(selectElement.value).toBe('private');
- fireEvent.change(selectElement, { target: { value: 'all_users', name: 'visibilitySocialLinks' } });
- expect(changeHandler).toHaveBeenCalledTimes(2);
-
- fireEvent.submit(container.querySelector('[aria-labelledby="editing-form"]'));
- expect(submitHandler).toHaveBeenCalledTimes(1);
-
- fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
- expect(closeHandler).toHaveBeenCalledTimes(1);
- });
-
- it('calls social links with static', () => {
- const openHandler = jest.fn();
- render(
- ,
- );
- const addFacebookButton = screen.getByRole('button', { name: 'Add Facebook' });
- fireEvent.click(addFacebookButton);
-
- expect(openHandler).toHaveBeenCalledTimes(1);
- });
-
- it('calls social links with error', () => {
- const newStore = JSON.parse(JSON.stringify(savingEditedBio));
- newStore.profilePage.errors.bio = { userMessage: 'error' };
-
- const { container } = render( );
-
- const alertDanger = container.querySelector('.alert-danger');
- expect(alertDanger).toBeInTheDocument();
- });
-});
diff --git a/src/profile/forms/__snapshots__/SocialLinks.test.jsx.snap b/src/profile/forms/__snapshots__/SocialLinks.test.jsx.snap
deleted file mode 100644
index 22ba79c..0000000
--- a/src/profile/forms/__snapshots__/SocialLinks.test.jsx.snap
+++ /dev/null
@@ -1,504 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[` calls social links with edit mode bio 1`] = `
-
-`;
-
-exports[` calls social links with edit mode certificates 1`] = `
-
-`;
-
-exports[` calls social links with edit mode goals 1`] = `
-
-
-
-
-
-
-
-
-
-
-
- Add Facebook
-
-
-
-
-
-
-
-
-
- Add Twitter
-
-
-
-
-
-
-
-`;
-
-exports[` calls social links with edit mode socialLinks 1`] = `
-
-`;
diff --git a/src/profile/forms/elements/EditButton.jsx b/src/profile/forms/elements/EditButton.jsx
index 278b34d..80cb479 100644
--- a/src/profile/forms/elements/EditButton.jsx
+++ b/src/profile/forms/elements/EditButton.jsx
@@ -1,25 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
+import { EditOutline } from '@openedx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { Button } from '@openedx/paragon';
-
+import { Button, OverlayTrigger, Tooltip } from '@openedx/paragon';
import messages from './EditButton.messages';
const EditButton = ({
onClick, className, style, intl,
}) => (
-
+
+ {intl.formatMessage(messages['profile.editbutton.edit'])}
+
+
+ )}
>
-
- {intl.formatMessage(messages['profile.editbutton.edit'])}
-
+
+
+
+
);
export default injectIntl(EditButton);
@@ -28,8 +37,6 @@ EditButton.propTypes = {
onClick: PropTypes.func.isRequired,
className: PropTypes.string,
style: PropTypes.object, // eslint-disable-line
-
- // i18n
intl: intlShape.isRequired,
};
diff --git a/src/profile/forms/elements/EditableItemHeader.jsx b/src/profile/forms/elements/EditableItemHeader.jsx
index 7b4b7b5..5381e3a 100644
--- a/src/profile/forms/elements/EditableItemHeader.jsx
+++ b/src/profile/forms/elements/EditableItemHeader.jsx
@@ -1,8 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
+import classNames from 'classnames';
import EditButton from './EditButton';
import { Visibility } from './Visibility';
+import { useIsOnMobileScreen } from '../../data/hooks';
const EditableItemHeader = ({
content,
@@ -11,15 +13,39 @@ const EditableItemHeader = ({
showEditButton,
onClickEdit,
headingId,
-}) => (
-
-
- {content}
- {showEditButton ? : null}
-
- {showVisibility ?
: null}
-
-);
+}) => {
+ const isMobileView = useIsOnMobileScreen();
+ return (
+ <>
+
+
+
+ {content}
+
+
+
+ {showEditButton ? : null}
+
+
+
+ {showVisibility ?
: null}
+
+ >
+ );
+};
export default EditableItemHeader;
@@ -33,7 +59,8 @@ EditableItemHeader.propTypes = {
};
EditableItemHeader.defaultProps = {
- onClickEdit: () => {},
+ onClickEdit: () => {
+ },
showVisibility: false,
showEditButton: false,
content: '',
diff --git a/src/profile/forms/elements/EmptyContent.jsx b/src/profile/forms/elements/EmptyContent.jsx
index 1af947c..3978d80 100644
--- a/src/profile/forms/elements/EmptyContent.jsx
+++ b/src/profile/forms/elements/EmptyContent.jsx
@@ -4,16 +4,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
const EmptyContent = ({ children, onClick, showPlusIcon }) => (
-
+
{onClick ? (
{ if (e.key === 'Enter') { onClick(); } }}
tabIndex={0}
>
- {showPlusIcon ? : null}
+ {showPlusIcon ? : null}
{children}
) : children}
diff --git a/src/profile/forms/elements/FormControls.jsx b/src/profile/forms/elements/FormControls.jsx
index e1fcaac..ddb2866 100644
--- a/src/profile/forms/elements/FormControls.jsx
+++ b/src/profile/forms/elements/FormControls.jsx
@@ -6,38 +6,47 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './FormControls.messages';
import { VisibilitySelect } from './Visibility';
+import { useIsVisibilityEnabled } from '../../data/hooks';
const FormControls = ({
cancelHandler, changeHandler, visibility, visibilityId, saveState, intl,
}) => {
- // Eliminate error/failed state for save button
const buttonState = saveState === 'error' ? null : saveState;
+ const isVisibilityEnabled = useIsVisibilityEnabled();
return (
-
-
- {intl.formatMessage(messages['profile.formcontrols.who.can.see'])}
-
-
-
-
-
{
+ {isVisibilityEnabled && (
+
+
+ {intl.formatMessage(messages['profile.formcontrols.who.can.see'])}
+
+
+
+ )}
+
+
+
+ {intl.formatMessage(messages['profile.formcontrols.button.cancel'])}
+
+
+
+ {
// Swallow clicks if the state is pending.
// We do this instead of disabling the button to prevent
// it from losing focus (disabled elements cannot have focus).
@@ -45,15 +54,13 @@ const FormControls = ({
// Swallowing the onSubmit event on the form would be better, but
// we would have to add that logic for every field given our
// current structure of the application.
- if (buttonState === 'pending') {
- e.preventDefault();
- }
- }}
- disabledStates={[]}
- />
-
- {intl.formatMessage(messages['profile.formcontrols.button.cancel'])}
-
+ if (buttonState === 'pending') {
+ e.preventDefault();
+ }
+ }}
+ disabledStates={[]}
+ />
+
);
@@ -68,7 +75,6 @@ FormControls.propTypes = {
cancelHandler: PropTypes.func.isRequired,
changeHandler: PropTypes.func.isRequired,
- // i18n
intl: intlShape.isRequired,
};
diff --git a/src/profile/forms/elements/SwitchContent.jsx b/src/profile/forms/elements/SwitchContent.jsx
index 1b45296..3bb1de5 100644
--- a/src/profile/forms/elements/SwitchContent.jsx
+++ b/src/profile/forms/elements/SwitchContent.jsx
@@ -3,18 +3,13 @@ import PropTypes from 'prop-types';
import { TransitionReplace } from '@openedx/paragon';
const onChildExit = (htmlNode) => {
- // If the leaving child has focus, take control and redirect it
if (htmlNode.contains(document.activeElement)) {
- // Get the newly entering sibling.
- // It's the previousSibling, but not for any explicit reason. So checking for both.
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
- // There's no replacement, do nothing.
if (!enteringChild) {
return;
}
- // Get all the focusable elements in the entering child and focus the first one
const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusableElements.length) {
focusableElements[0].focus();
diff --git a/src/profile/forms/elements/Visibility.jsx b/src/profile/forms/elements/Visibility.jsx
index cd59680..cfe67a2 100644
--- a/src/profile/forms/elements/Visibility.jsx
+++ b/src/profile/forms/elements/Visibility.jsx
@@ -23,7 +23,6 @@ const Visibility = ({ to, intl }) => {
Visibility.propTypes = {
to: PropTypes.oneOf(['private', 'all_users']),
- // i18n
intl: intlShape.isRequired,
};
Visibility.defaultProps = {
@@ -36,7 +35,7 @@ const VisibilitySelect = ({ intl, className, ...props }) => {
return (
-
+
@@ -58,7 +57,6 @@ VisibilitySelect.propTypes = {
value: PropTypes.oneOf(['private', 'all_users']),
onChange: PropTypes.func,
- // i18n
intl: intlShape.isRequired,
};
VisibilitySelect.defaultProps = {
diff --git a/src/profile/index.scss b/src/profile/index.scss
index bee9a81..87907c2 100644
--- a/src/profile/index.scss
+++ b/src/profile/index.scss
@@ -17,29 +17,13 @@
}
.profile-page-bg-banner {
- height: 12rem;
+ height: 298px;
+ width: 100%;
background-image: url('./assets/dot-pattern-light.png');
background-repeat: repeat-x;
background-size: auto 85%;
}
-.username-description {
- width: auto;
- position: absolute;
- left: 1.5rem;
- top: 5.25rem;
- color: var(--pgn-color-gray-500);
- line-height: 0.9rem;
- font-size: 0.8rem;
- font-style: normal;
- font-weight: 400;
- margin-left: 0.9rem;
-}
-
-.mb-2rem {
- margin-bottom: 2rem;
-}
-
.icon-visibility-off {
height: 1rem;
color: var(--pgn-color-gray-500);
@@ -47,11 +31,12 @@
.profile-page {
.edit-section-header {
- font-size: var(--pgn-typography-font-size-h6-base);
+ font-size: var(--pgn-typography-font-size-h4-base);
display: block;
- font-weight: normal;
+ font-weight: 400;
letter-spacing: 0;
margin: 0;
+ line-height: 2.25rem;
}
label.edit-section-header {
@@ -62,11 +47,16 @@
@media (--pgn-size-breakpoint-min-width-md) {
max-width: 12rem;
margin-right: 0;
- margin-top: -8rem;
- margin-bottom: 2rem;
+ height: auto;
}
}
+ .profile-avatar-button {
+ position: absolute;
+ left: 76px;
+ top: 76px;
+ }
+
.profile-avatar-menu-container {
background: rgba(0,0,0,.65);
position: absolute;
@@ -77,7 +67,7 @@
align-items: center;
border-radius: 50%;
- @media (--pgn-size-breakpoint-min-width-md) {
+ @media (--pgn-size-breakpoint-min-width-md) {
background: linear-gradient(to top, rgba(0,0,0,.65) 4rem, rgba(0,0,0,0) 4rem);
align-items: flex-end;
}
@@ -95,7 +85,7 @@
}
.btn {
- color: var(--pgn-color-white);
+ color:var(--pgn-color-white);;
background: transparent;
border-color: transparent;
margin: 0;
@@ -104,13 +94,13 @@
}
.profile-avatar {
- width: 5rem;
- height: 5rem;
+ width: 7.5rem;
+ height: 7.5rem;
position: relative;
@media (--pgn-size-breakpoint-min-width-md) {
- width: 12rem;
- height: 12rem;
+ width: 7.5rem;
+ height: 7.5rem;
}
.profile-avatar-edit-button {
@@ -139,27 +129,137 @@
}
.certificate {
- position: relative;
-
- .certificate-title {
- font-family: var(--pgn-typography-font-family-serif);
- font-weight: 400;
- }
+ background-color: #F3F1ED;
+ border-radius: 0.75rem;
+ overflow: hidden;
+ border: 1px #E7E4DB solid;
.certificate-type-illustration {
position: absolute;
top: 1rem;
right: 1rem;
bottom: 0;
- width: 12rem;
+ width: 15.15rem;
opacity: .06;
background-size: 90%;
background-repeat: no-repeat;
background-position: right top;
}
-
- .card-body {
- position: relative;
- }
}
}
+
+.info-icon {
+ width: 1.5rem;
+ height: 1.5rem;
+ padding-left: 0.125rem;
+}
+
+.max-width-32em {
+ max-width: 32em;
+}
+
+.height-50vh {
+ height: 50vh;
+}
+
+// Todo: Move the following to edx-paragon
+
+.btn-rounded {
+ border-radius: 100px;
+}
+
+.min-width-179px {
+ min-width: 179px;
+}
+
+.max-width-304px{
+ max-width: 304px;
+}
+
+.width-314px {
+ width: 314px;
+}
+
+.w-90{
+ max-width: 90%;
+}
+
+.width-24px{
+ width: 24px;
+}
+
+.height-42px {
+ height: 42px;
+}
+
+.rounded-75 {
+ border-radius: 0.75rem;
+}
+
+.pt-40px{
+ padding-top: 40px;
+}
+
+.pl-40px {
+ padding-left: 40px;
+}
+
+.py-10px{
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+.py-36px {
+ padding-top: 36px;
+ padding-bottom: 36px;
+}
+
+.px-120px {
+ padding-left: 120px;
+ padding-right: 120px;
+}
+
+.px-40px {
+ padding-left: 40px;
+ padding-right: 40px;
+}
+
+.g-15rem {
+ gap: 1.5rem;
+}
+
+.g-5rem {
+ gap: 0.5rem;
+}
+
+.g-1rem {
+ gap: 1rem;
+}
+
+.g-3rem {
+ gap: 3rem;
+}
+
+.color-black {
+ color: #000;
+}
+
+.bg-color-grey-FBFAF9 {
+ background-color: #FBFAF9;
+}
+
+.background-black-65 {
+ background-color: rgba(0,0,0,.65)
+}
+
+.object-fit-cover {
+ object-fit: cover;
+}
+
+.lh-36px {
+ line-height: 36px;
+}
+
+.overflowWrap-breakWord {
+ overflow-wrap: break-word;
+}
diff --git a/src/profile/utils.js b/src/profile/utils.js
index 29981a6..4a68d4d 100644
--- a/src/profile/utils.js
+++ b/src/profile/utils.js
@@ -2,7 +2,6 @@ import camelCase from 'lodash.camelcase';
import snakeCase from 'lodash.snakecase';
export function modifyObjectKeys(object, modify) {
- // If the passed in object is not an object, return it.
if (
object === undefined
|| object === null
@@ -15,7 +14,6 @@ export function modifyObjectKeys(object, modify) {
return object.map(value => modifyObjectKeys(value, modify));
}
- // Otherwise, process all its keys.
const result = {};
Object.entries(object).forEach(([key, value]) => {
result[modify(key)] = modifyObjectKeys(value, modify);
diff --git a/src/routes/AppRoutes.jsx b/src/routes/AppRoutes.jsx
index 5743a09..8fead5a 100644
--- a/src/routes/AppRoutes.jsx
+++ b/src/routes/AppRoutes.jsx
@@ -1,30 +1,18 @@
import React from 'react';
-import PropTypes from 'prop-types';
import { AuthenticatedPageRoute, PageWrap } from '@edx/frontend-platform/react';
import { Routes, Route, useNavigate } from 'react-router-dom';
import { ProfilePage, NotFoundPage } from '../profile';
-import { ProfilePage as NewProfilePage, NotFoundPage as NewNotFoundPage } from '../profile-v2';
-const AppRoutes = ({ isNewProfileEnabled }) => {
- const SelectedProfilePage = isNewProfileEnabled ? NewProfilePage : ProfilePage;
- const SelectedNotFoundPage = isNewProfileEnabled ? NewNotFoundPage : NotFoundPage;
+const AppRoutes = () => {
const navigate = useNavigate();
return (
- } />
- } />
- } />
+ } />
+ } />
+ } />
);
};
-AppRoutes.propTypes = {
- isNewProfileEnabled: PropTypes.bool,
-};
-
-AppRoutes.defaultProps = {
- isNewProfileEnabled: null,
-};
-
export default AppRoutes;
diff --git a/src/routes/routes.test.jsx b/src/routes/routes.test.jsx
index 3b2b622..9bd40df 100644
--- a/src/routes/routes.test.jsx
+++ b/src/routes/routes.test.jsx
@@ -17,11 +17,6 @@ jest.mock('../profile', () => ({
NotFoundPage: () => (Not found page
),
}));
-jest.mock('../profile-v2', () => ({
- ProfilePage: () => (Profile page
),
- NotFoundPage: () => (Not found page
),
-}));
-
const RoutesWithProvider = (context, path) => (