Compare commits

..

12 Commits

Author SHA1 Message Date
Awais Ansari
02681b7e34 test: add form fields test cases 2025-06-18 19:11:35 +05:00
Awais Ansari
f249c7fed4 test: add test cases for NotFoundPage 2025-06-18 18:45:53 +05:00
Eemaan Amir
8d2bd721f8 feat: made fullname uneditable and added redirect link (#1215)
* feat: made fullname uneditable and added redirect link

* refactor: refactored code

* refactor: refactored code
2025-06-10 18:25:45 +05:00
Eemaan Amir
82ce644d8f feat: made profile editable (#1212)
* feat: readded editable fields to new profile view

* feat: made fullname editable and updated design

* test: updated test cases

* refactor: refactored code based on reviews
2025-05-30 13:19:17 +05:00
sundasnoreen12
bf5dd5b556 Merge pull request #1185 from openedx/sundas/restricted-countries-changes
feat: added restricted country functionality
2025-03-19 16:15:38 +05:00
Awais Ansari
896f2cec1f test: updated test cases 2025-03-18 23:31:20 +05:00
sundasnoreen12
3fffbfefa6 test: updated snapshot 2025-03-18 22:10:44 +05:00
sundasnoreen12
7e3571e88b fix: fixed test cases 2025-03-18 21:58:03 +05:00
sundasnoreen12
29edfa9099 feat: added restricted country functionality 2025-03-18 21:53:41 +05:00
Awais Ansari
adb12b4e21 Merge pull request #1184 from openedx/aansari/sync-with-master
chore: sync with master
2025-03-18 19:11:24 +05:00
Awais Ansari
2216c1b9cc test: updated test cases 2025-03-18 16:53:08 +05:00
Awais Ansari
acfcfb495c Merge branch 'master' of github.com:openedx/frontend-app-profile into aansari/sync-with-master 2025-03-18 16:21:53 +05:00
70 changed files with 6729 additions and 4244 deletions

1
.env
View File

@@ -30,3 +30,4 @@ MFE_CONFIG_API_URL=''
SEARCH_CATALOG_URL=''
ENABLE_SKILLS_BUILDER_PROFILE=''
ENABLE_NEW_PROFILE_VIEW=''
DISABLE_VISIBILITY_EDITING=''

View File

@@ -31,3 +31,4 @@ MFE_CONFIG_API_URL=''
SEARCH_CATALOG_URL='http://localhost:18000/courses'
ENABLE_SKILLS_BUILDER_PROFILE=''
ENABLE_NEW_PROFILE_VIEW=''
DISABLE_VISIBILITY_EDITING=''

View File

@@ -26,3 +26,4 @@ COLLECT_YEAR_OF_BIRTH=true
APP_ID=''
MFE_CONFIG_API_URL=''
ENABLE_NEW_PROFILE_VIEW=''
DISABLE_VISIBILITY_EDITING=''

10
package-lock.json generated
View File

@@ -2354,7 +2354,6 @@
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.3.1.tgz",
"integrity": "sha512-wDCCFtbWdxk8N/ExIGd/etyidF9YewaRdyGix2nSTujdfKZU/+2cObRxGkardGHREQDGrvqCPW5tmcSNedAIIg==",
"license": "AGPL-3.0",
"dependencies": {
"@cospired/i18n-iso-languages": "4.2.0",
"@formatjs/intl-pluralrules": "4.3.3",
@@ -2395,7 +2394,6 @@
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@@ -2407,7 +2405,6 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -2427,7 +2424,6 @@
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
@@ -15562,9 +15558,9 @@
}
},
"node_modules/nwsapi": {
"version": "2.2.18",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.18.tgz",
"integrity": "sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA=="
"version": "2.2.19",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.19.tgz",
"integrity": "sha512-94bcyI3RsqiZufXjkr3ltkI86iEl+I7uiHVDtcq9wJUTwYQJ5odHDeSzkkrRzi80jJ8MaeZgqKjH1bAWAFw9bA=="
},
"node_modules/object-assign": {
"version": "4.1.1",

View File

@@ -36,7 +36,11 @@
"dateJoined": "2017-06-07T00:44:23Z",
"email": "staff@example.com",
"isActive": true,
"languageProficiencies": [],
"levelOfEducation": null,
"name": "Lemon Seltzer",
"profileImage": {},
"socialLinks": [],
"username": "staff",
"yearOfBirth": 1901
},

View File

@@ -43,8 +43,8 @@ const CertificateCard = ({
/>
<div className={classNames(
'd-flex flex-column position-relative p-0',
{ 'max-width-19rem': isMobileView },
{ 'width-19625rem': !isMobileView },
{ 'max-width-304px': isMobileView },
{ 'width-314px': !isMobileView },
)}
>
<div className="w-100 color-black">
@@ -97,7 +97,7 @@ const CertificateCard = ({
target="_blank"
showLaunchIcon={false}
className={classNames(
'btn btn-primary btn-rounded font-weight-normal px-4 py-0625rem',
'btn btn-primary btn-rounded font-weight-normal px-4 py-10px',
{ 'btn-sm': isMobileView },
)}
>

View File

@@ -7,20 +7,15 @@ import { getConfig } from '@edx/frontend-platform';
import classNames from 'classnames';
import CertificateCard from './CertificateCard';
import { certificatesSelector } from './data/selectors';
import { useIsOnMobileScreen, useIsOnTabletScreen } from './data/hooks';
import { useIsOnTabletScreen } from './data/hooks';
const Certificates = ({ certificates }) => {
const isMobileView = useIsOnMobileScreen();
const isTabletView = useIsOnTabletScreen();
return (
<div>
<div className="col justify-content-start align-items-start g-5rem p-0">
<div className="col align-self-stretch height-2625rem justify-content-start align-items-start p-0">
<p className={classNames([
'font-weight-bold text-primary-500 m-0',
isMobileView ? 'h3' : 'h2',
])}
>
<div className="col align-self-stretch height-42px justify-content-start align-items-start p-0">
<p className="font-weight-bold text-primary-500 m-0 h2">
<FormattedMessage
id="profile.your.certificates"
defaultMessage="Your certificates"
@@ -29,14 +24,10 @@ const Certificates = ({ certificates }) => {
</p>
</div>
<div className="col justify-content-start align-items-start pt-2 p-0">
<p className={classNames([
'font-weight-normal text-gray-800 m-0 p-0',
isMobileView ? 'h5' : 'p',
])}
>
<p className="font-weight-normal text-gray-800 m-0 p-0 p">
<FormattedMessage
id="profile.certificates.description"
defaultMessage="Your learner records information is only visible to you. Only your username is visible to others on {siteName}."
defaultMessage="Your learner records information is only visible to you. Only your username and profile image are visible to others on {siteName}."
description="description of the certificates section"
values={{
siteName: getConfig().SITE_NAME,

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import NotFoundPage from './NotFoundPage';
describe('NotFoundPage Snapshot Tests', () => {
it('renders correctly', () => {
const { asFragment } = render(
<IntlProvider locale="en">
<NotFoundPage />
</IntlProvider>,
);
expect(asFragment()).toMatchSnapshot();
});
it('renders with custom props', () => {
const { asFragment } = render(
<IntlProvider locale="en">
<NotFoundPage message="Custom not found message" />
</IntlProvider>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@@ -3,46 +3,76 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';
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 { useIntl } 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';
import {
fetchProfile,
saveProfile,
saveProfilePhoto,
deleteProfilePhoto,
openForm,
closeForm,
updateDraft,
} from './data/actions';
import ProfileAvatar from './forms/ProfileAvatar';
import Certificates from './Certificates';
import Name from './forms/Name';
import Country from './forms/Country';
import PreferredLanguage from './forms/PreferredLanguage';
import Education from './forms/Education';
import SocialLinks from './forms/SocialLinks';
import Bio from './forms/Bio';
import DateJoined from './DateJoined';
import UserCertificateSummary from './UserCertificateSummary';
import UsernameDescription from './UsernameDescription';
import PageLoading from './PageLoading';
import Certificates from './Certificates';
import { profilePageSelector } from './data/selectors';
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');
const ProfilePage = ({ params }) => {
const dispatch = useDispatch();
const intl = useIntl();
const context = useContext(AppContext);
const {
requiresParentalConsent,
dateJoined,
yearOfBirth,
courseCertificates,
name,
visibilityName,
profileImage,
savePhotoState,
isLoadingProfile,
photoUploadError,
country,
visibilityCountry,
levelOfEducation,
visibilityLevelOfEducation,
socialLinks,
draftSocialLinksByPlatform,
visibilitySocialLinks,
languageProficiencies,
visibilityLanguageProficiencies,
bio,
visibilityBio,
saveState,
username,
} = useSelector(profilePageSelector);
const navigate = useNavigate();
const [viewMyRecordsUrl, setViewMyRecordsUrl] = useState(null);
const isMobileView = useIsOnMobileScreen();
const isTabletView = useIsOnTabletScreen();
@@ -59,21 +89,39 @@ const ProfilePage = ({ params }) => {
});
}, [dispatch, params.username, context.config]);
useEffect(() => {
if (!username && saveState === 'error' && navigate) {
navigate('/notfound');
}
}, [username, saveState, navigate]);
const authenticatedUserName = context.authenticatedUser.username;
const handleSaveProfilePhoto = useCallback((formData) => {
dispatch(saveProfilePhoto(context.authenticatedUser.username, formData));
}, [dispatch, context.authenticatedUser.username]);
dispatch(saveProfilePhoto(authenticatedUserName, formData));
}, [dispatch, authenticatedUserName]);
const handleDeleteProfilePhoto = useCallback(() => {
dispatch(deleteProfilePhoto(context.authenticatedUser.username));
}, [dispatch, context.authenticatedUser.username]);
dispatch(deleteProfilePhoto(authenticatedUserName));
}, [dispatch, authenticatedUserName]);
const isYOBDisabled = () => {
const currentYear = new Date().getFullYear();
const isAgeOrNotCompliant = !yearOfBirth || ((currentYear - yearOfBirth) < 13);
return isAgeOrNotCompliant && getConfig().COLLECT_YEAR_OF_BIRTH !== 'true';
};
const handleClose = useCallback((formId) => {
dispatch(closeForm(formId));
}, [dispatch]);
const isAuthenticatedUserProfile = () => params.username === context.authenticatedUser.username;
const handleOpen = useCallback((formId) => {
dispatch(openForm(formId));
}, [dispatch]);
const handleSubmit = useCallback((formId) => {
dispatch(saveProfile(formId, authenticatedUserName));
}, [dispatch, authenticatedUserName]);
const handleChange = useCallback((fieldName, value) => {
dispatch(updateDraft(fieldName, value));
}, [dispatch]);
const isAuthenticatedUserProfile = () => params.username === authenticatedUserName;
const isBlockVisible = (blockInfo) => isAuthenticatedUserProfile()
|| (!isAuthenticatedUserProfile() && Boolean(blockInfo));
@@ -86,8 +134,8 @@ const ProfilePage = ({ params }) => {
return (
<Hyperlink
className={classNames(
'btn btn-brand btn-rounded font-weight-normal px-4 py-0625rem text-nowrap',
{ 'btn-sm': isMobileView },
'btn btn-brand bg-brand-500 btn-rounded font-weight-normal px-4 py-10px text-nowrap',
{ 'w-100': isMobileView },
)}
target="_blank"
showLaunchIcon={false}
@@ -100,16 +148,23 @@ const ProfilePage = ({ params }) => {
const renderPhotoUploadErrorMessage = () => (
photoUploadError && (
<div className="row">
<div className="col-md-4 col-lg-3">
<Alert variant="danger" dismissible={false} show>
{photoUploadError.userMessage}
</Alert>
<div className="row">
<div className="col-md-4 col-lg-3">
<Alert variant="danger" dismissible={false} show>
{photoUploadError.userMessage}
</Alert>
</div>
</div>
</div>
)
);
const commonFormProps = {
openHandler: handleOpen,
closeHandler: handleClose,
submitHandler: handleSubmit,
changeHandler: handleChange,
};
return (
<div className="profile-page">
{isLoadingProfile ? (
@@ -118,13 +173,29 @@ const ProfilePage = ({ params }) => {
<>
<div
className={classNames(
'profile-page-bg-banner bg-primary d-md-block align-items-center py-4rem h-100 w-100',
{ 'px-4.5': isMobileView },
{ 'px-75rem': !isMobileView },
'profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100',
{ 'px-3 py-4': isMobileView },
{ 'px-120px py-5.5': !isMobileView },
)}
>
<div className="col container-fluid w-100 h-100 bg-white py-0 px-25rem rounded-75">
<div className="col h-100 w-100 py-4 px-0 justify-content-start g-15rem">
<div
className={classNames([
'col container-fluid w-100 h-100 bg-white py-0 rounded-75',
{
'px-3': isMobileView,
'px-40px': !isMobileView,
},
])}
>
<div
className={classNames([
'col h-100 w-100 px-0 justify-content-start g-15rem',
{
'py-4': isMobileView,
'py-36px': !isMobileView,
},
])}
>
<div
className={classNames([
'row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem',
@@ -138,7 +209,7 @@ const ProfilePage = ({ params }) => {
onSave={handleSaveProfilePhoto}
onDelete={handleDeleteProfilePhoto}
savePhotoState={savePhotoState}
isEditable={isAuthenticatedUserProfile() && !requiresParentalConsent}
isEditable={isAuthenticatedUserProfile()}
/>
<div
className={classNames([
@@ -148,19 +219,11 @@ const ProfilePage = ({ params }) => {
: 'justify-content-start align-items-start',
])}
>
<p className={classNames([
'row m-0 font-weight-bold text-truncate text-primary-500',
isMobileView ? 'h4' : 'h3',
])}
>
<p className="row m-0 font-weight-bold text-truncate text-primary-500 h3">
{params.username}
</p>
{isBlockVisible(name) && (
<p className={classNames([
'row pt-2 text-gray-800 font-weight-normal m-0',
isMobileView ? 'h5' : 'p',
])}
>
<p className="row pt-2 text-gray-800 font-weight-normal m-0 p">
{name}
</p>
)}
@@ -172,7 +235,7 @@ const ProfilePage = ({ params }) => {
)}
>
<DateJoined date={dateJoined} />
<UserCertificateSummary count={courseCertificates.length} />
<UserCertificateSummary count={courseCertificates?.length || 0} />
</div>
</div>
<div className={classNames([
@@ -183,21 +246,146 @@ const ProfilePage = ({ params }) => {
{renderViewMyRecordsButton()}
</div>
</div>
<div className="row-auto d-flex align-items-center h-100 w-100 justify-content-start m-0 pt-4">
{isYOBDisabled() && <UsernameDescription />}
</div>
</div>
<div className="ml-auto">
{renderPhotoUploadErrorMessage()}
</div>
</div>
</div>
<div className="col container-fluid d-inline-flex px-75rem pt-4rem pb-6 h-100 w-100 align-items-start justify-content-center g-3rem">
{isBlockVisible(courseCertificates.length) && (
<Certificates
certificates={courseCertificates}
formId="certificates"
/>
<div
className={classNames([
'col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem',
isMobileView ? 'py-4 px-3' : 'px-120px py-6',
])}
>
<div className="w-100 p-0">
<div className="col justify-content-start align-items-start p-0">
<div className="col align-self-stretch height-42px justify-content-start align-items-start p-0">
<p className="font-weight-bold text-primary-500 m-0 h2">
{isMobileView ? (
<FormattedMessage
id="profile.profile.information"
defaultMessage="Profile"
description="heading for the editable profile section in mobile view"
/>
)
: (
<FormattedMessage
id="profile.profile.information"
defaultMessage="Profile information"
description="heading for the editable profile section"
/>
)}
</p>
</div>
</div>
<div
className={classNames([
'row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start',
isMobileView ? 'pt-4' : 'pt-5.5',
])}
>
<div
className={classNames([
'col p-0',
isMobileView ? 'col-12' : 'col-6',
])}
>
<div className="m-0">
<div className="row m-0 pb-1.5 align-items-center">
<p data-hj-suppress className="h5 font-weight-bold m-0">
{intl.formatMessage(messages['profile.username'])}
</p>
<OverlayTrigger
key="top"
placement="top"
overlay={(
<Tooltip variant="light" id="tooltip-top">
<p className="h5 font-weight-normal m-0 p-0">
{intl.formatMessage(messages['profile.username.tooltip'])}
</p>
</Tooltip>
)}
>
<InfoOutline className="m-0 info-icon" />
</OverlayTrigger>
</div>
<h4 className="edit-section-header text-gray-700">
{params.username}
</h4>
</div>
{isBlockVisible(name) && (
<Name
name={name}
accountSettingsUrl={context.config.ACCOUNT_SETTINGS_URL}
visibilityName={visibilityName}
formId="name"
{...commonFormProps}
/>
)}
{isBlockVisible(country) && (
<Country
country={country}
visibilityCountry={visibilityCountry}
formId="country"
{...commonFormProps}
/>
)}
{isBlockVisible((languageProficiencies || []).length) && (
<PreferredLanguage
languageProficiencies={languageProficiencies || []}
visibilityLanguageProficiencies={visibilityLanguageProficiencies}
formId="languageProficiencies"
{...commonFormProps}
/>
)}
{isBlockVisible(levelOfEducation) && (
<Education
levelOfEducation={levelOfEducation}
visibilityLevelOfEducation={visibilityLevelOfEducation}
formId="levelOfEducation"
{...commonFormProps}
/>
)}
</div>
<div
className={classNames([
'col m-0 pr-0',
isMobileView ? 'pl-0 col-12' : 'pl-40px col-6',
])}
>
{isBlockVisible(bio) && (
<Bio
bio={bio}
visibilityBio={visibilityBio}
formId="bio"
{...commonFormProps}
/>
)}
{isBlockVisible((socialLinks || []).some((link) => link?.socialLink !== null)) && (
<SocialLinks
socialLinks={socialLinks || []}
draftSocialLinksByPlatform={draftSocialLinksByPlatform || {}}
visibilitySocialLinks={visibilitySocialLinks}
formId="socialLinks"
{...commonFormProps}
/>
)}
</div>
</div>
</div>
</div>
<div
className={classNames([
'col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem',
isMobileView ? 'py-4 px-3' : 'px-120px py-6',
])}
>
{isBlockVisible((courseCertificates || []).length) && (
<Certificates
certificates={courseCertificates || []}
formId="certificates"
/>
)}
</div>
</>
@@ -210,6 +398,66 @@ ProfilePage.propTypes = {
params: PropTypes.shape({
username: PropTypes.string.isRequired,
}).isRequired,
requiresParentalConsent: PropTypes.bool,
dateJoined: PropTypes.string,
username: PropTypes.string,
bio: PropTypes.string,
visibilityBio: PropTypes.string,
courseCertificates: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string,
})),
country: PropTypes.string,
visibilityCountry: PropTypes.string,
levelOfEducation: PropTypes.string,
visibilityLevelOfEducation: PropTypes.string,
languageProficiencies: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
})),
visibilityLanguageProficiencies: PropTypes.string,
name: PropTypes.string,
visibilityName: PropTypes.string,
socialLinks: PropTypes.arrayOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
})),
draftSocialLinksByPlatform: PropTypes.objectOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
})),
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,
photoUploadError: PropTypes.objectOf(PropTypes.string),
};
ProfilePage.defaultProps = {
saveState: null,
username: '',
savePhotoState: null,
photoUploadError: {},
profileImage: {},
name: null,
levelOfEducation: null,
country: null,
socialLinks: [],
draftSocialLinksByPlatform: {},
bio: null,
languageProficiencies: [],
courseCertificates: [],
requiresParentalConsent: null,
dateJoined: null,
visibilityName: null,
visibilityCountry: null,
visibilityLevelOfEducation: null,
visibilitySocialLinks: null,
visibilityLanguageProficiencies: null,
visibilityBio: null,
isLoadingProfile: false,
};
export default withParams(ProfilePage);

View File

@@ -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;

View File

@@ -8,12 +8,24 @@ import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
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]);
@@ -21,14 +33,13 @@ const storeMocks = {
loadingApp,
viewOwnProfile,
viewOtherProfile,
invalidUser,
};
const requiredProfilePageProps = {
fetchUserAccount: () => {},
fetchProfile: () => {},
params: { username: 'staff' },
};
// Mock language cookie
Object.defineProperty(global.document, 'cookie', {
writable: true,
value: `${getConfig().LANGUAGE_PREFERENCE_COOKIE_NAME}=en`,
@@ -60,30 +71,39 @@ configureI18n({
beforeEach(() => {
analytics.sendTrackingLogEvent.mockReset();
useNavigate.mockReset();
});
const ProfilePageWrapper = ({
contextValue, store, params,
}) => (
<AppContext.Provider
value={contextValue}
>
<AppContext.Provider value={contextValue}>
<IntlProvider locale="en">
<Provider store={store}>
<ProfilePage {...requiredProfilePageProps} params={params} />
<MemoryRouter initialEntries={[`/profile/${params.username}`]}>
<Routes>
<Route
path="/profile/:username"
element={<ProfilePage {...requiredProfilePageProps} params={params} />}
/>
</Routes>
</MemoryRouter>
</Provider>
</IntlProvider>
</AppContext.Provider>
);
ProfilePageWrapper.defaultProps = {
// eslint-disable-next-line react/default-props-match-prop-types
params: { username: 'staff' },
};
ProfilePageWrapper.propTypes = {
contextValue: PropTypes.shape({}).isRequired,
store: PropTypes.shape({}).isRequired,
params: PropTypes.shape({}),
params: PropTypes.shape({
username: PropTypes.string.isRequired,
}).isRequired,
};
describe('<ProfilePage />', () => {
@@ -93,7 +113,12 @@ describe('<ProfilePage />', () => {
authenticatedUser: { userId: null, username: null, administrator: false },
config: getConfig(),
};
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.loadingApp)} />;
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.loadingApp)}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
@@ -103,7 +128,12 @@ describe('<ProfilePage />', () => {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.viewOwnProfile)} />;
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOwnProfile)}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
@@ -113,7 +143,6 @@ describe('<ProfilePage />', () => {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
@@ -123,19 +152,26 @@ describe('<ProfilePage />', () => {
...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
params={{ username: 'verified' }}
/>
);
const { container: tree } = render(component);
@@ -159,6 +195,25 @@ describe('<ProfilePage />', () => {
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 navigate = jest.fn();
useNavigate.mockReturnValue(navigate);
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.invalidUser)}
params={{ username: 'staffTest' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
expect(navigate).toHaveBeenCalledWith('/notfound');
});
});
describe('handles analytics', () => {
@@ -175,11 +230,287 @@ describe('<ProfilePage />', () => {
/>,
);
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(
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.invalidUser)}
params={{ username: 'staffTest' }}
/>,
);
expect(navigate).toHaveBeenCalledWith('/notfound');
});
});
describe('form fields', () => {
it('renders all form fields for own profile', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const { getByText } = render(
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOwnProfile)}
/>,
);
expect(getByText('Full name')).toBeInTheDocument();
expect(getByText('Country')).toBeInTheDocument();
expect(getByText('Bio')).toBeInTheDocument();
expect(getByText('Education')).toBeInTheDocument();
expect(getByText('Primary language spoken')).toBeInTheDocument();
});
});
describe('handles invalid user', () => {
it('navigates to not found page for invalid user', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const navigate = jest.fn();
useNavigate.mockReturnValue(navigate);
render(
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.invalidUser)}
params={{ username: 'invalidUser' }}
/>,
);
expect(navigate).toHaveBeenCalledWith('/notfound');
});
});
describe('handles empty profile', () => {
it('renders empty profile state', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'empty' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only username', () => {
it('renders profile with only username', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyUsername' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with no social links', () => {
it('renders profile without social links', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'noSocialLinks' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only social links', () => {
it('renders profile with only social links', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlySocialLinks' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only bio', () => {
it('renders profile with only bio', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyBio' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only country', () => {
it('renders profile with only country', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyCountry' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only level of education', () => {
it('renders profile with only level of education', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyLevelOfEducation' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only language proficiencies', () => {
it('renders profile with only language proficiencies', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyLanguageProficiencies' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only course certificates', () => {
it('renders profile with only course certificates', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyCourseCertificates' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only name', () => {
it('renders profile with only name', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyName' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only username and no other fields', () => {
it('renders profile with only username', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyUsernameNoFields' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with no fields and no username', () => {
it('renders empty profile state', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: '' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
});

View File

@@ -2,18 +2,23 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
const UserCertificateSummary = ({ count = 0 }) => (
<span className="small m-0 text-gray-800">
<FormattedMessage
id="profile.certificatecount"
defaultMessage="{certificate_count} certifications"
description="A label for many certificates a user has"
values={{
certificate_count: <span className="font-weight-bold"> {count} </span>,
}}
/>
</span>
);
const UserCertificateSummary = ({ count = 0 }) => {
if (count) {
return (
<span className="small m-0 text-gray-800">
<FormattedMessage
id="profile.certificatecount"
defaultMessage="{certificate_count} certifications"
description="A label for many certificates a user has"
values={{
certificate_count: <span className="font-weight-bold">{count}</span>,
}}
/>
</span>
);
}
return null;
};
UserCertificateSummary.propTypes = {
count: PropTypes.number,

View File

@@ -1,27 +0,0 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import classNames from 'classnames';
import { useIsOnMobileScreen } from './data/hooks';
const UsernameDescription = () => {
const isMobileView = useIsOnMobileScreen();
return (
<p className={classNames([
'text-gray-800 font-weight-normal m-0',
isMobileView ? 'h5' : 'p',
])}
>
<FormattedMessage
id="profile.username.description"
defaultMessage="Your learner records information is only visible to you. Only your username is visible to others on {siteName}."
description="A description of the username field"
values={{
siteName: getConfig().SITE_NAME,
}}
/>
</p>
);
};
export default UsernameDescription;

View File

@@ -0,0 +1,42 @@
module.exports = {
userAccount: {
loading: false,
error: null,
username: 'staff',
email: null,
bio: null,
name: null,
country: null,
socialLinks: null,
profileImage: {
imageUrlMedium: null,
imageUrlLarge: null
},
levelOfEducation: null,
learningGoal: null
},
profilePage: {
errors: {},
saveState: 'error',
savePhotoState: null,
currentlyEditingField: null,
account: {
username: '',
socialLinks: []
},
preferences: {},
courseCertificates: [],
drafts: {},
isLoadingProfile: false,
isAuthenticatedUserProfile: true,
countriesCodesList: ['US', 'CA', 'GB', 'ME']
},
router: {
location: {
pathname: '/u/staffTest',
search: '',
hash: ''
},
action: 'POP'
}
};

View File

@@ -29,6 +29,7 @@ module.exports = {
drafts: {},
isLoadingProfile: true,
isAuthenticatedUserProfile: true,
countriesCodesList: ['US', 'CA', 'GB', 'ME']
},
router: {
location: {

View File

@@ -81,11 +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: ['US', 'CA', 'GB', 'ME']
},
router: {
location: {

View File

@@ -125,7 +125,8 @@ module.exports = {
}
],
drafts: {},
isLoadingProfile: false
isLoadingProfile: false,
countriesCodesList: ['US', 'CA', 'GB', 'ME']
},
router: {
location: {

View File

@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotFoundPage Snapshot Tests renders correctly 1`] = `
<DocumentFragment>
<div
class="container-fluid d-flex py-5 justify-content-center align-items-start text-center"
>
<p
class="my-0 py-5 text-muted max-width-32em"
>
The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.
</p>
</div>
</DocumentFragment>
`;
exports[`NotFoundPage Snapshot Tests renders with custom props 1`] = `
<DocumentFragment>
<div
class="container-fluid d-flex py-5 justify-content-center align-items-start text-center"
>
<p
class="my-0 py-5 text-muted max-width-32em"
>
The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.
</p>
</div>
</DocumentFragment>
`;

View File

@@ -25,19 +25,19 @@ exports[`<ProfilePage /> Renders correctly in various states app loading 1`] = `
</div>
`;
exports[`<ProfilePage /> Renders correctly in various states viewing other profile with all fields 1`] = `
exports[`<ProfilePage /> Renders correctly in various states successfully redirected to not found page 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center py-4rem h-100 w-100 px-75rem"
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 px-25rem rounded-75"
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 py-4 px-0 justify-content-start g-15rem"
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
@@ -48,9 +48,10 @@ exports[`<ProfilePage /> Renders correctly in various states viewing other profi
<div
class="profile-avatar rounded-circle bg-light"
>
<iconmock
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
@@ -74,12 +75,149 @@ exports[`<ProfilePage /> Renders correctly in various states viewing other profi
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
staff
staffTest
</p>
<div
class="row pt-2 m-0 g-1rem"
/>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
staffTest
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> Renders correctly in various states viewing other profile with all fields 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
verified
</p>
<p
class="row pt-2 text-gray-800 font-weight-normal m-0 p"
>
user
Verified User
</p>
<div
class="row pt-2 m-0 g-1rem"
@@ -96,41 +234,11 @@ exports[`<ProfilePage /> Renders correctly in various states viewing other profi
</span>
</span>
<span
class="small m-0 text-gray-800"
>
<span
class="font-weight-bold"
>
0
</span>
certifications
</span>
</div>
</div>
<div
class="p-0 col-auto"
>
<a
class="pgn__hyperlink default-link standalone-link btn btn-brand btn-rounded font-weight-normal px-4 py-0625rem text-nowrap"
href="http://localhost:18150/records"
rel="noopener noreferrer"
target="_blank"
>
View My Records
</a>
</div>
</div>
<div
class="row-auto d-flex align-items-center h-100 w-100 justify-content-start m-0 pt-4"
>
<p
class="text-gray-800 font-weight-normal m-0 p"
>
Your learner records information is only visible to you. Only your username is visible to others on localhost.
</p>
/>
</div>
</div>
<div
@@ -139,38 +247,296 @@ exports[`<ProfilePage /> Renders correctly in various states viewing other profi
</div>
</div>
<div
class="col container-fluid d-inline-flex px-75rem pt-4rem pb-6 h-100 w-100 align-items-start justify-content-center g-3rem"
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start g-5rem p-0"
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-2625rem justify-content-start align-items-start p-0"
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Your certificates
</p>
</div>
<div
class="col justify-content-start align-items-start pt-2 p-0"
>
<p
class="font-weight-normal text-gray-800 m-0 p-0 p"
>
Your learner records information is only visible to you. Only your username is visible to others on localhost.
Profile information
</p>
</div>
</div>
<div
class="pt-5"
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
You don't have any certificates yet.
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
verified
</h4>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Full name
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Verified User
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
/>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Country
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
United States of America
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
/>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Primary language spoken
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
English
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
/>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Education
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Other education
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
/>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
>
<div
class="pgn-transition-replace-group position-relative pt-0"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Bio
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
About me
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
/>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative p-0"
>
<div
style="padding: .1px 0px;"
>
<div>
<div>
<div
class="pt-40px"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
X
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
https://twitter.com/user
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
/>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
@@ -181,13 +547,13 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center py-4rem h-100 w-100 px-75rem"
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 px-25rem rounded-75"
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 py-4 px-0 justify-content-start g-15rem"
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
@@ -198,23 +564,6 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
<div
class="profile-avatar rounded-circle bg-light"
>
<div
class="profile-avatar-menu-container"
>
<div
class="pgn__dropdown pgn__dropdown-light dropdown"
data-testid="dropdown"
>
<button
aria-expanded="false"
aria-haspopup="true"
class="dropdown-toggle btn btn-primary"
type="button"
>
Change
</button>
</div>
</div>
<img
alt="profile avatar"
class="w-100 h-100 d-block rounded-circle overflow-hidden object-fit-cover"
@@ -222,6 +571,50 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
src="http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012"
/>
</div>
<div
class="profile-avatar-button"
>
<div
class="pgn__dropdown pgn__dropdown-light dropdown"
data-testid="dropdown"
>
<button
aria-expanded="false"
aria-haspopup="true"
class="btn-icon btn-icon-inverse-primary btn-icon-md btn-icon-inverse-primary-active shadow-sm pgn__dropdown-toggle-iconbutton"
id="dropdown-toggle-with-iconbutton"
type="button"
>
<span
class="btn-icon__icon-container"
>
<span
class="pgn__icon btn-icon__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"
fill="currentColor"
/>
<path
d="M9 2 7.17 4H2v16h20V4h-5.17L15 2H9Zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5Z"
fill="currentColor"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
<form
enctype="multipart/form-data"
>
@@ -268,9 +661,7 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
<span
class="font-weight-bold"
>
1
</span>
certifications
</span>
@@ -280,7 +671,7 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
class="p-0 col-auto"
>
<a
class="pgn__hyperlink default-link standalone-link btn btn-brand btn-rounded font-weight-normal px-4 py-0625rem text-nowrap"
class="pgn__hyperlink default-link standalone-link btn btn-brand bg-brand-500 btn-rounded font-weight-normal px-4 py-10px text-nowrap"
href="http://localhost:18150/records"
rel="noopener noreferrer"
target="_blank"
@@ -289,9 +680,6 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
</a>
</div>
</div>
<div
class="row-auto d-flex align-items-center h-100 w-100 justify-content-start m-0 pt-4"
/>
</div>
<div
class="ml-auto"
@@ -299,14 +687,509 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
</div>
</div>
<div
class="col container-fluid d-inline-flex px-75rem pt-4rem pb-6 h-100 w-100 align-items-start justify-content-center g-3rem"
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
staff
</h4>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Full name
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Lemon Seltzer
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Country
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Montenegro
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Primary language spoken
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Yoruba
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Education
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Elementary/primary school
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
>
<div
class="pgn-transition-replace-group position-relative pt-0"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Bio
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
This is my bio
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative p-0"
>
<div
style="padding: .1px 0px;"
>
<div>
<div>
<div
class="pt-40px"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
X
</p>
<div
class="w-100 overflowWrap-breakWord"
>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
https://www.twitter.com/ALOHA
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pt-40px"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Facebook
</p>
<div
class="w-100 overflowWrap-breakWord"
>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
https://www.facebook.com/aloha
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pt-40px"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
LinkedIn
</p>
<div
class="p-0 m-0"
>
<button
class="p-0 text-left btn btn-link lh-36px"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-plus fa-xs mr-1"
data-icon="plus"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"
fill="currentColor"
/>
</svg>
Add
LinkedIn
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div>
<div
class="col justify-content-start align-items-start g-5rem p-0"
>
<div
class="col align-self-stretch height-2625rem justify-content-start align-items-start p-0"
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
@@ -320,7 +1203,7 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
<p
class="font-weight-normal text-gray-800 m-0 p-0 p"
>
Your learner records information is only visible to you. Only your username is visible to others on localhost.
Your learner records information is only visible to you. Only your username and profile image are visible to others on localhost.
</p>
</div>
</div>
@@ -341,7 +1224,7 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
style="background-image: url(icon/mock/path);"
/>
<div
class="d-flex flex-column position-relative p-0 width-19625rem"
class="d-flex flex-column position-relative p-0 width-314px"
>
<div
class="w-100 color-black"
@@ -377,7 +1260,7 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
class="pt-3"
>
<a
class="pgn__hyperlink default-link standalone-link btn btn-primary btn-rounded font-weight-normal px-4 py-0625rem"
class="pgn__hyperlink default-link standalone-link btn btn-primary btn-rounded font-weight-normal px-4 py-10px"
href="http://www.example.com/"
rel="noopener noreferrer"
target="_blank"
@@ -407,13 +1290,13 @@ exports[`<ProfilePage /> Renders correctly in various states without credentials
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center py-4rem h-100 w-100 px-75rem"
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 px-25rem rounded-75"
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 py-4 px-0 justify-content-start g-15rem"
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
@@ -424,23 +1307,6 @@ exports[`<ProfilePage /> Renders correctly in various states without credentials
<div
class="profile-avatar rounded-circle bg-light"
>
<div
class="profile-avatar-menu-container"
>
<div
class="pgn__dropdown pgn__dropdown-light dropdown"
data-testid="dropdown"
>
<button
aria-expanded="false"
aria-haspopup="true"
class="dropdown-toggle btn btn-primary"
type="button"
>
Change
</button>
</div>
</div>
<img
alt="profile avatar"
class="w-100 h-100 d-block rounded-circle overflow-hidden object-fit-cover"
@@ -448,6 +1314,50 @@ exports[`<ProfilePage /> Renders correctly in various states without credentials
src="http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012"
/>
</div>
<div
class="profile-avatar-button"
>
<div
class="pgn__dropdown pgn__dropdown-light dropdown"
data-testid="dropdown"
>
<button
aria-expanded="false"
aria-haspopup="true"
class="btn-icon btn-icon-inverse-primary btn-icon-md btn-icon-inverse-primary-active shadow-sm pgn__dropdown-toggle-iconbutton"
id="dropdown-toggle-with-iconbutton"
type="button"
>
<span
class="btn-icon__icon-container"
>
<span
class="pgn__icon btn-icon__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"
fill="currentColor"
/>
<path
d="M9 2 7.17 4H2v16h20V4h-5.17L15 2H9Zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5Z"
fill="currentColor"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
<form
enctype="multipart/form-data"
>
@@ -494,9 +1404,7 @@ exports[`<ProfilePage /> Renders correctly in various states without credentials
<span
class="font-weight-bold"
>
1
</span>
certifications
</span>
@@ -506,9 +1414,6 @@ exports[`<ProfilePage /> Renders correctly in various states without credentials
class="p-0 col-auto"
/>
</div>
<div
class="row-auto d-flex align-items-center h-100 w-100 justify-content-start m-0 pt-4"
/>
</div>
<div
class="ml-auto"
@@ -516,14 +1421,509 @@ exports[`<ProfilePage /> Renders correctly in various states without credentials
</div>
</div>
<div
class="col container-fluid d-inline-flex px-75rem pt-4rem pb-6 h-100 w-100 align-items-start justify-content-center g-3rem"
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
staff
</h4>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Full name
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Lemon Seltzer
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Country
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Montenegro
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Primary language spoken
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Yoruba
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Education
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Elementary/primary school
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
>
<div
class="pgn-transition-replace-group position-relative pt-0"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Bio
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
This is my bio
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative p-0"
>
<div
style="padding: .1px 0px;"
>
<div>
<div>
<div
class="pt-40px"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
X
</p>
<div
class="w-100 overflowWrap-breakWord"
>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
https://www.twitter.com/ALOHA
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pt-40px"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Facebook
</p>
<div
class="w-100 overflowWrap-breakWord"
>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
https://www.facebook.com/aloha
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pt-40px"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
LinkedIn
</p>
<div
class="p-0 m-0"
>
<button
class="p-0 text-left btn btn-link lh-36px"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-plus fa-xs mr-1"
data-icon="plus"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"
fill="currentColor"
/>
</svg>
Add
LinkedIn
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div>
<div
class="col justify-content-start align-items-start g-5rem p-0"
>
<div
class="col align-self-stretch height-2625rem justify-content-start align-items-start p-0"
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
@@ -537,7 +1937,7 @@ exports[`<ProfilePage /> Renders correctly in various states without credentials
<p
class="font-weight-normal text-gray-800 m-0 p-0 p"
>
Your learner records information is only visible to you. Only your username is visible to others on localhost.
Your learner records information is only visible to you. Only your username and profile image are visible to others on localhost.
</p>
</div>
</div>
@@ -558,7 +1958,7 @@ exports[`<ProfilePage /> Renders correctly in various states without credentials
style="background-image: url(icon/mock/path);"
/>
<div
class="d-flex flex-column position-relative p-0 width-19625rem"
class="d-flex flex-column position-relative p-0 width-314px"
>
<div
class="w-100 color-black"
@@ -594,7 +1994,7 @@ exports[`<ProfilePage /> Renders correctly in various states without credentials
class="pt-3"
>
<a
class="pgn__hyperlink default-link standalone-link btn btn-primary btn-rounded font-weight-normal px-4 py-0625rem"
class="pgn__hyperlink default-link standalone-link btn btn-primary btn-rounded font-weight-normal px-4 py-10px"
href="http://www.example.com/"
rel="noopener noreferrer"
target="_blank"
@@ -617,3 +2017,1655 @@ exports[`<ProfilePage /> Renders correctly in various states without credentials
</div>
</div>
`;
exports[`<ProfilePage /> handles empty profile renders empty profile state 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
empty
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
empty
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with no fields and no username renders empty profile state 1`] = `<div />`;
exports[`<ProfilePage /> handles profile with no social links renders profile without social links 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
noSocialLinks
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
noSocialLinks
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only bio renders profile with only bio 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyBio
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyBio
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only country renders profile with only country 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyCountry
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyCountry
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only course certificates renders profile with only course certificates 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyCourseCertificates
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyCourseCertificates
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only language proficiencies renders profile with only language proficiencies 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyLanguageProficiencies
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyLanguageProficiencies
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only level of education renders profile with only level of education 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyLevelOfEducation
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyLevelOfEducation
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only name renders profile with only name 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyName
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyName
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only social links renders profile with only social links 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlySocialLinks
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlySocialLinks
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only username and no other fields renders profile with only username 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyUsernameNoFields
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyUsernameNoFields
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only username renders profile with only username 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyUsername
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyUsername
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;

View File

@@ -1,10 +1,13 @@
import { AsyncActionType } from '../utils';
export const FETCH_PROFILE = new AsyncActionType('PROFILE', 'FETCH_PROFILE');
export const SAVE_PROFILE = new AsyncActionType('PROFILE', 'SAVE_PROFILE');
export const SAVE_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'SAVE_PROFILE_PHOTO');
export const DELETE_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'DELETE_PROFILE_PHOTO');
// FETCH PROFILE ACTIONS
export const OPEN_FORM = 'OPEN_FORM';
export const CLOSE_FORM = 'CLOSE_FORM';
export const UPDATE_DRAFT = 'UPDATE_DRAFT';
export const RESET_DRAFTS = 'RESET_DRAFTS';
export const fetchProfile = username => ({
type: FETCH_PROFILE.BASE,
@@ -32,7 +35,34 @@ export const fetchProfileReset = () => ({
type: FETCH_PROFILE.RESET,
});
// SAVE PROFILE PHOTO ACTIONS
export const saveProfile = (formId, username) => ({
type: SAVE_PROFILE.BASE,
payload: {
formId,
username,
},
});
export const saveProfileBegin = () => ({
type: SAVE_PROFILE.BEGIN,
});
export const saveProfileSuccess = (account, preferences) => ({
type: SAVE_PROFILE.SUCCESS,
payload: {
account,
preferences,
},
});
export const saveProfileReset = () => ({
type: SAVE_PROFILE.RESET,
});
export const saveProfileFailure = errors => ({
type: SAVE_PROFILE.FAILURE,
payload: { errors },
});
export const saveProfilePhoto = (username, formData) => ({
type: SAVE_PROFILE_PHOTO.BASE,
@@ -60,8 +90,6 @@ export const saveProfilePhotoFailure = error => ({
payload: { error },
});
// DELETE PROFILE PHOTO ACTIONS
export const deleteProfilePhoto = username => ({
type: DELETE_PROFILE_PHOTO.BASE,
payload: {
@@ -81,3 +109,29 @@ export const deleteProfilePhotoSuccess = profileImage => ({
export const deleteProfilePhotoReset = () => ({
type: DELETE_PROFILE_PHOTO.RESET,
});
export const openForm = formId => ({
type: OPEN_FORM,
payload: {
formId,
},
});
export const closeForm = formId => ({
type: CLOSE_FORM,
payload: {
formId,
},
});
export const updateDraft = (name, value) => ({
type: UPDATE_DRAFT,
payload: {
name,
value,
},
});
export const resetDrafts = () => ({
type: RESET_DRAFTS,
});

View File

@@ -22,7 +22,12 @@ const SOCIAL = {
},
};
const FIELD_LABELS = {
COUNTRY: 'country',
};
export {
EDUCATION_LEVELS,
SOCIAL,
FIELD_LABELS,
};

View File

@@ -1,4 +1,5 @@
import { breakpoints, useWindowSize } from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
export function useIsOnTabletScreen() {
const windowSize = useWindowSize();
@@ -9,3 +10,25 @@ export function useIsOnMobileScreen() {
const windowSize = useWindowSize();
return windowSize.width <= breakpoints.small.minWidth;
}
export function useIsVisibilityEnabled() {
return getConfig().DISABLE_VISIBILITY_EDITING === 'true';
}
export function useHandleChange(changeHandler) {
return (e) => {
const { name, value } = e.target;
changeHandler(name, value);
};
}
export function useHandleSubmit(submitHandler, formId) {
return (e) => {
e.preventDefault();
submitHandler(formId);
};
}
export function useCloseOpenHandler(handler, formId) {
return () => handler(formId);
}

View File

@@ -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({

View File

@@ -1,20 +1,43 @@
import {
SAVE_PROFILE,
SAVE_PROFILE_PHOTO,
DELETE_PROFILE_PHOTO,
CLOSE_FORM,
OPEN_FORM,
FETCH_PROFILE,
UPDATE_DRAFT,
RESET_DRAFTS,
} from './actions';
export const initialState = {
errors: {},
saveState: null,
savePhotoState: null,
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: [],
};
const profilePage = (state = initialState, action = {}) => {
@@ -30,13 +53,51 @@ 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 || [],
};
case SAVE_PROFILE.BEGIN:
return {
...state,
saveState: 'pending',
errors: {},
};
case SAVE_PROFILE.SUCCESS:
return {
...state,
saveState: 'complete',
errors: {},
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:
return {
...state,
saveState: 'error',
isLoadingProfile: false,
errors: { ...state.errors, ...action.payload.errors },
};
case SAVE_PROFILE.RESET:
return {
...state,
saveState: null,
isLoadingProfile: false,
errors: {},
};
case SAVE_PROFILE_PHOTO.BEGIN:
return {
...state,
@@ -46,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: {},
@@ -63,7 +123,6 @@ const profilePage = (state = initialState, action = {}) => {
savePhotoState: null,
errors: {},
};
case DELETE_PROFILE_PHOTO.BEGIN:
return {
...state,
@@ -73,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: {},
@@ -90,7 +148,31 @@ 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,
drafts: {},
};
case OPEN_FORM:
return {
...state,
currentlyEditingField: action.payload.formId,
drafts: {},
};
case CLOSE_FORM:
if (action.payload.formId === state.currentlyEditingField) {
return {
...state,
currentlyEditingField: null,
drafts: {},
};
}
return state;
default:
return state;
}

View File

@@ -1,8 +1,13 @@
import profilePage, { initialState } from './reducers';
import {
SAVE_PROFILE,
SAVE_PROFILE_PHOTO,
DELETE_PROFILE_PHOTO,
CLOSE_FORM,
OPEN_FORM,
FETCH_PROFILE,
UPDATE_DRAFT,
RESET_DRAFTS,
} from './actions';
describe('profilePage reducer', () => {
@@ -15,7 +20,6 @@ describe('profilePage reducer', () => {
const action = { type: FETCH_PROFILE.BEGIN };
const expectedState = {
...initialState,
// Uncomment isLoadingProfile: true if this functionality is required.
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
@@ -23,18 +27,112 @@ describe('profilePage reducer', () => {
it('should handle FETCH_PROFILE.SUCCESS', () => {
const action = {
type: FETCH_PROFILE.SUCCESS,
account: { name: 'John Doe' },
preferences: { theme: 'dark' },
account: {
name: 'John Doe',
bio: 'Software Engineer',
country: 'US',
levelOfEducation: 'bachelors',
socialLinks: [{ platform: 'twitter', link: 'twitter.com/johndoe' }],
languageProficiencies: [{ code: 'en', name: 'English' }],
profileImage: { url: 'profile.jpg' },
yearOfBirth: 1990,
},
preferences: {
visibilityName: 'public',
visibilityBio: 'public',
visibilityCountry: 'public',
visibilityLevelOfEducation: 'public',
visibilitySocialLinks: 'public',
visibilityLanguageProficiencies: 'public',
},
courseCertificates: ['cert1', 'cert2'],
isAuthenticatedUserProfile: true,
countriesCodesList: ['US', 'CA'],
};
const expectedState = {
...initialState,
account: action.account,
account: {
...initialState.account,
...action.account,
socialLinks: action.account.socialLinks,
languageProficiencies: action.account.languageProficiencies,
},
preferences: action.preferences,
courseCertificates: action.courseCertificates,
isLoadingProfile: false,
isAuthenticatedUserProfile: action.isAuthenticatedUserProfile,
countriesCodesList: action.countriesCodesList,
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
});
describe('SAVE_PROFILE actions', () => {
it('should handle SAVE_PROFILE.BEGIN', () => {
const action = { type: SAVE_PROFILE.BEGIN };
const expectedState = {
...initialState,
saveState: 'pending',
errors: {},
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle SAVE_PROFILE.SUCCESS', () => {
const action = {
type: SAVE_PROFILE.SUCCESS,
payload: {
account: {
name: 'Jane Doe',
bio: 'Updated bio',
socialLinks: [{ platform: 'linkedin', link: 'linkedin.com/janedoe' }],
languageProficiencies: [{ code: 'es', name: 'Spanish' }],
},
preferences: {
visibilityName: 'private',
visibilityBio: 'private',
},
},
};
const expectedState = {
...initialState,
saveState: 'complete',
errors: {},
account: {
...initialState.account,
...action.payload.account,
socialLinks: action.payload.account.socialLinks,
languageProficiencies: action.payload.account.languageProficiencies,
},
preferences: {
...initialState.preferences,
...action.payload.preferences,
},
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle SAVE_PROFILE.FAILURE', () => {
const action = {
type: SAVE_PROFILE.FAILURE,
payload: { errors: { save: 'Failed to save profile' } },
};
const expectedState = {
...initialState,
saveState: 'error',
isLoadingProfile: false,
errors: { save: action.payload.errors.save },
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle SAVE_PROFILE.RESET', () => {
const action = { type: SAVE_PROFILE.RESET };
const expectedState = {
...initialState,
saveState: null,
isLoadingProfile: false,
errors: {},
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
@@ -54,7 +152,7 @@ describe('profilePage reducer', () => {
it('should handle SAVE_PROFILE_PHOTO.SUCCESS', () => {
const action = {
type: SAVE_PROFILE_PHOTO.SUCCESS,
payload: { profileImage: 'new-image-url.jpg' },
payload: { profileImage: { url: 'new-image-url.jpg' } },
};
const expectedState = {
...initialState,
@@ -103,7 +201,7 @@ describe('profilePage reducer', () => {
it('should handle DELETE_PROFILE_PHOTO.SUCCESS', () => {
const action = {
type: DELETE_PROFILE_PHOTO.SUCCESS,
payload: { profileImage: 'default-image-url.jpg' },
payload: { profileImage: { url: 'default-image-url.jpg' } },
};
const expectedState = {
...initialState,
@@ -122,7 +220,7 @@ describe('profilePage reducer', () => {
const expectedState = {
...initialState,
savePhotoState: 'error',
errors: { ...initialState.errors, delete: action.payload.errors.delete },
errors: { delete: action.payload.errors.delete },
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
@@ -137,4 +235,75 @@ describe('profilePage reducer', () => {
expect(profilePage(initialState, action)).toEqual(expectedState);
});
});
describe('Draft and Form actions', () => {
it('should handle UPDATE_DRAFT', () => {
const action = {
type: UPDATE_DRAFT,
payload: { name: 'bio', value: 'New bio draft' },
};
const expectedState = {
...initialState,
drafts: { bio: 'New bio draft' },
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle RESET_DRAFTS', () => {
const initialStateWithDrafts = {
...initialState,
drafts: { bio: 'New bio draft', name: 'New name' },
};
const action = { type: RESET_DRAFTS };
const expectedState = {
...initialStateWithDrafts,
drafts: {},
};
expect(profilePage(initialStateWithDrafts, action)).toEqual(expectedState);
});
it('should handle OPEN_FORM', () => {
const action = {
type: OPEN_FORM,
payload: { formId: 'bioForm' },
};
const expectedState = {
...initialState,
currentlyEditingField: 'bioForm',
drafts: {},
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle CLOSE_FORM when formId matches currentlyEditingField', () => {
const initialStateWithForm = {
...initialState,
currentlyEditingField: 'bioForm',
drafts: { bio: 'New bio draft' },
};
const action = {
type: CLOSE_FORM,
payload: { formId: 'bioForm' },
};
const expectedState = {
...initialStateWithForm,
currentlyEditingField: null,
drafts: {},
};
expect(profilePage(initialStateWithForm, action)).toEqual(expectedState);
});
it('should not handle CLOSE_FORM when formId does not match currentlyEditingField', () => {
const initialStateWithForm = {
...initialState,
currentlyEditingField: 'bioForm',
drafts: { bio: 'New bio draft' },
};
const action = {
type: CLOSE_FORM,
payload: { formId: 'nameForm' },
};
expect(profilePage(initialStateWithForm, action)).toEqual(initialStateWithForm);
});
});
});

View File

@@ -1,13 +1,16 @@
import { history } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import pick from 'lodash.pick';
import {
all,
call,
delay,
put,
select,
takeEvery,
} from 'redux-saga/effects';
import {
closeForm,
deleteProfilePhotoBegin,
deleteProfilePhotoReset,
deleteProfilePhotoSuccess,
@@ -16,50 +19,50 @@ import {
fetchProfileReset,
fetchProfileSuccess,
FETCH_PROFILE,
resetDrafts,
saveProfileBegin,
saveProfileFailure,
saveProfileReset,
saveProfileSuccess,
SAVE_PROFILE,
saveProfilePhotoBegin,
saveProfilePhotoReset,
saveProfilePhotoSuccess,
SAVE_PROFILE_PHOTO,
} from './actions';
import { userAccountSelector } from './selectors';
import { handleSaveProfileSelector, userAccountSelector } from './selectors';
import * as ProfileApiService from './services';
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;
let countriesCodesList = [];
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),
call(ProfileApiService.getCountryList),
];
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) {
[account, courseCertificates, preferences] = result;
[account, courseCertificates, countriesCodesList, preferences] = result;
} else {
[account, courseCertificates] = result;
[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',
@@ -80,6 +83,7 @@ export function* handleFetchProfile(action) {
preferences,
courseCertificates,
isAuthenticatedUserProfile,
countriesCodesList,
));
yield put(fetchProfileReset());
@@ -92,6 +96,67 @@ export function* handleFetchProfile(action) {
}
}
export function* handleSaveProfile(action) {
try {
const { drafts, preferences } = yield select(handleSaveProfileSelector);
const accountDrafts = pick(drafts, [
'bio',
'country',
'levelOfEducation',
'languageProficiencies',
'name',
'socialLinks',
]);
const preferencesDrafts = pick(drafts, [
'visibilityBio',
'visibilityCountry',
'visibilityLevelOfEducation',
'visibilityLanguageProficiencies',
'visibilityName',
'visibilitySocialLinks',
]);
if (Object.keys(preferencesDrafts).length > 0) {
preferencesDrafts.accountPrivacy = 'custom';
}
yield put(saveProfileBegin());
let accountResult = null;
if (Object.keys(accountDrafts).length > 0) {
accountResult = yield call(
ProfileApiService.patchProfile,
action.payload.username,
accountDrafts,
);
}
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.
preferencesResult = yield call(ProfileApiService.getPreferences, action.payload.username);
}
yield put(saveProfileSuccess(accountResult, preferencesResult));
yield delay(1000);
yield put(closeForm(action.payload.formId));
yield delay(300);
yield put(saveProfileReset());
yield put(resetDrafts());
} catch (e) {
if (e.processedData && e.processedData.fieldErrors) {
yield put(saveProfileFailure(e.processedData.fieldErrors));
} else {
yield put(saveProfileReset());
throw e;
}
}
}
export function* handleSaveProfilePhoto(action) {
const { username, formData } = action.payload;
@@ -101,7 +166,6 @@ export function* handleSaveProfilePhoto(action) {
yield put(saveProfilePhotoSuccess(photoResult));
yield put(saveProfilePhotoReset());
} catch (e) {
// Just reset on error, since editing functionality is deprecated
yield put(saveProfilePhotoReset());
}
}
@@ -115,13 +179,13 @@ export function* handleDeleteProfilePhoto(action) {
yield put(deleteProfilePhotoSuccess(photoResult));
yield put(deleteProfilePhotoReset());
} catch (e) {
// Just reset on error, since editing functionality is deprecated
yield put(deleteProfilePhotoReset());
}
}
export default function* profileSaga() {
yield takeEvery(FETCH_PROFILE.BASE, handleFetchProfile);
yield takeEvery(SAVE_PROFILE.BASE, handleSaveProfile);
yield takeEvery(SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto);
yield takeEvery(DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto);
}

View File

@@ -2,39 +2,40 @@ import {
takeEvery,
put,
call,
delay,
select,
all,
} from 'redux-saga/effects';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import * as profileActions from './actions';
import { userAccountSelector } from './selectors';
import profileSaga, {
handleFetchProfile,
handleSaveProfilePhoto,
handleDeleteProfilePhoto,
} from './sagas';
import * as ProfileApiService from './services';
import {
deleteProfilePhotoBegin,
deleteProfilePhotoReset,
saveProfilePhotoBegin,
saveProfilePhotoReset,
} from './actions';
import { handleSaveProfileSelector, userAccountSelector } from './selectors';
jest.mock('./services', () => ({
getAccount: jest.fn(),
getCourseCertificates: jest.fn(),
getPreferences: jest.fn(),
getProfile: jest.fn(),
patchProfile: jest.fn(),
postProfilePhoto: jest.fn(),
deleteProfilePhoto: jest.fn(),
getPreferences: jest.fn(),
getAccount: jest.fn(),
getCourseCertificates: jest.fn(),
getCountryList: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(),
}));
/* eslint-disable import/first */
import profileSaga, {
handleFetchProfile,
handleSaveProfile,
handleSaveProfilePhoto,
handleDeleteProfilePhoto,
} from './sagas';
import * as ProfileApiService from './services';
/* eslint-enable import/first */
describe('RootSaga', () => {
describe('profileSaga', () => {
it('should pass actions to the correct sagas', () => {
@@ -42,6 +43,8 @@ describe('RootSaga', () => {
expect(gen.next().value)
.toEqual(takeEvery(profileActions.FETCH_PROFILE.BASE, handleFetchProfile));
expect(gen.next().value)
.toEqual(takeEvery(profileActions.SAVE_PROFILE.BASE, handleSaveProfile));
expect(gen.next().value)
.toEqual(takeEvery(profileActions.SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto));
expect(gen.next().value)
@@ -65,17 +68,18 @@ describe('RootSaga', () => {
const action = profileActions.fetchProfile('gonzo');
const gen = handleFetchProfile(action);
const result = [userAccount, [1, 2, 3], { preferences: 'stuff' }];
const result = [userAccount, [1, 2, 3], [], { preferences: 'stuff' }];
expect(gen.next().value).toEqual(select(userAccountSelector));
expect(gen.next(selectorData).value).toEqual(put(profileActions.fetchProfileBegin()));
expect(gen.next().value).toEqual(all([
call(ProfileApiService.getAccount, 'gonzo'),
call(ProfileApiService.getCourseCertificates, 'gonzo'),
call(ProfileApiService.getCountryList),
call(ProfileApiService.getPreferences, 'gonzo'),
]));
expect(gen.next(result).value)
.toEqual(put(profileActions.fetchProfileSuccess(userAccount, result[2], result[1], true)));
.toEqual(put(profileActions.fetchProfileSuccess(userAccount, result[3], result[1], true, [])));
expect(gen.next().value).toEqual(put(profileActions.fetchProfileReset()));
expect(gen.next().value).toBeUndefined();
});
@@ -85,6 +89,7 @@ describe('RootSaga', () => {
username: 'gonzo',
other: 'data',
};
const countriesCodesList = [{ code: 'AX' }, { code: 'AL' }];
getAuthenticatedUser.mockReturnValue(userAccount);
const selectorData = {
userAccount,
@@ -93,66 +98,69 @@ describe('RootSaga', () => {
const action = profileActions.fetchProfile('booyah');
const gen = handleFetchProfile(action);
const result = [{}, [1, 2, 3]];
const result = [{}, [1, 2, 3], countriesCodesList];
expect(gen.next().value).toEqual(select(userAccountSelector));
expect(gen.next(selectorData).value).toEqual(put(profileActions.fetchProfileBegin()));
expect(gen.next().value).toEqual(all([
call(ProfileApiService.getAccount, 'booyah'),
call(ProfileApiService.getCourseCertificates, 'booyah'),
call(ProfileApiService.getCountryList),
]));
expect(gen.next(result).value)
.toEqual(put(profileActions.fetchProfileSuccess(result[0], {}, result[1], false)));
.toEqual(put(profileActions.fetchProfileSuccess(result[0], {}, result[1], false, countriesCodesList)));
expect(gen.next().value).toEqual(put(profileActions.fetchProfileReset()));
expect(gen.next().value).toBeUndefined();
});
});
describe('handleSaveProfilePhoto', () => {
it('should publish a reset action on error', () => {
const action = profileActions.saveProfilePhoto('my username', {});
const gen = handleSaveProfilePhoto(action);
const error = new Error('Error occurred');
describe('handleSaveProfile', () => {
const selectorData = {
username: 'my username',
drafts: {
name: 'Full Name',
},
preferences: {},
};
expect(gen.next().value).toEqual(put(saveProfilePhotoBegin()));
expect(gen.throw(error).value).toEqual(put(saveProfilePhotoReset()));
expect(gen.next().value).toBeUndefined();
});
});
describe('handleDeleteProfilePhoto', () => {
it('should publish a reset action on error', () => {
const action = profileActions.deleteProfilePhoto('my username');
const gen = handleDeleteProfilePhoto(action);
const error = new Error('Error occurred');
expect(gen.next().value).toEqual(put(deleteProfilePhotoBegin()));
expect(gen.throw(error).value).toEqual(put(deleteProfilePhotoReset()));
expect(gen.next().value).toBeUndefined();
});
});
describe('handleDeleteProfilePhoto', () => {
it('should successfully process a deleteProfilePhoto request if there are no exceptions', () => {
const action = profileActions.deleteProfilePhoto('my username');
const gen = handleDeleteProfilePhoto(action);
const photoResult = {};
expect(gen.next().value).toEqual(put(profileActions.deleteProfilePhotoBegin()));
expect(gen.next().value).toEqual(call(ProfileApiService.deleteProfilePhoto, 'my username'));
expect(gen.next(photoResult).value).toEqual(put(profileActions.deleteProfilePhotoSuccess(photoResult)));
expect(gen.next().value).toEqual(put(profileActions.deleteProfilePhotoReset()));
it('should successfully process a saveProfile request if there are no exceptions', () => {
const action = profileActions.saveProfile('ze form id', 'my username');
const gen = handleSaveProfile(action);
const profile = {
name: 'Full Name',
levelOfEducation: 'b',
};
expect(gen.next().value).toEqual(select(handleSaveProfileSelector));
expect(gen.next(selectorData).value).toEqual(put(profileActions.saveProfileBegin()));
expect(gen.next().value).toEqual(call(ProfileApiService.patchProfile, 'my username', {
name: 'Full Name',
}));
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')));
expect(gen.next().value).toEqual(delay(300));
expect(gen.next().value).toEqual(put(profileActions.saveProfileReset()));
expect(gen.next().value).toEqual(put(profileActions.resetDrafts()));
expect(gen.next().value).toBeUndefined();
});
it('should publish a failure action on exception', () => {
const error = new Error('Error occurred');
const action = profileActions.deleteProfilePhoto('my username');
const gen = handleDeleteProfilePhoto(action);
it('should successfully publish a failure action on exception', () => {
const error = new Error('uhoh');
error.processedData = {
fieldErrors: {
uhoh: 'not good',
},
};
const action = profileActions.saveProfile(
'ze form id',
'my username',
);
const gen = handleSaveProfile(action);
expect(gen.next().value).toEqual(put(profileActions.deleteProfilePhotoBegin()));
expect(gen.next().value).toEqual(select(handleSaveProfileSelector));
expect(gen.next(selectorData).value).toEqual(put(profileActions.saveProfileBegin()));
const result = gen.throw(error);
expect(result.value).toEqual(put(profileActions.deleteProfilePhotoReset()));
expect(result.value).toEqual(put(profileActions.saveProfileFailure({ uhoh: 'not good' })));
expect(gen.next().value).toBeUndefined();
});
});

View File

@@ -1,15 +1,138 @@
import { createSelector } from 'reselect';
import {
getLocale,
getLanguageList,
getCountryList,
getCountryMessages,
getLanguageMessages,
} 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 saveStateSelector = state => state.profilePage.saveState;
export const savePhotoStateSelector = state => state.profilePage.savePhotoState;
export const isLoadingProfileSelector = state => state.profilePage.isLoadingProfile;
export const currentlyEditingFieldSelector = state => state.profilePage.currentlyEditingField;
export const accountErrorsSelector = state => state.profilePage.errors;
export const isAuthenticatedUserProfileSelector = state => state.profilePage.isAuthenticatedUserProfile;
export const countriesCodesListSelector = state => state.profilePage.countriesCodesList;
export const editableFormModeSelector = createSelector(
profileAccountSelector,
isAuthenticatedUserProfileSelector,
profileCourseCertificatesSelector,
formIdSelector,
currentlyEditingFieldSelector,
(account, isAuthenticatedUserProfile, certificates, formId, currentlyEditingField) => {
let propExists = account[formId] != null && account[formId].length > 0;
propExists = formId === 'certificates' ? certificates.length > 0 : propExists;
if (!isAuthenticatedUserProfile) {
return 'static';
}
if (formId === currentlyEditingField) {
return 'editing';
}
if (!propExists) {
return 'empty';
}
return 'editable';
},
);
export const accountDraftsFieldSelector = createSelector(
formIdSelector,
profileDraftsSelector,
(formId, drafts) => drafts[formId],
);
export const visibilityDraftsFieldSelector = createSelector(
formIdSelector,
profileDraftsSelector,
(formId, drafts) => drafts[`visibility${formId.charAt(0).toUpperCase() + formId.slice(1)}`],
);
export const formErrorSelector = createSelector(
accountErrorsSelector,
formIdSelector,
(errors, formId) => (errors[formId] ? errors[formId].userMessage : null),
);
export const editableFormSelector = createSelector(
editableFormModeSelector,
formErrorSelector,
saveStateSelector,
(editMode, error, saveState) => ({
editMode,
error,
saveState,
}),
);
export const localeSelector = () => getLocale();
export const countryMessagesSelector = createSelector(
localeSelector,
locale => getCountryMessages(locale),
);
export const languageMessagesSelector = createSelector(
localeSelector,
locale => getLanguageMessages(locale),
);
export const sortedLanguagesSelector = createSelector(
localeSelector,
locale => getLanguageList(locale),
);
export const sortedCountriesSelector = createSelector(
localeSelector,
countriesCodesListSelector,
profileAccountSelector,
(locale, countriesCodesList, profileAccount) => {
const countryList = getCountryList(locale);
const userCountry = profileAccount.country;
return countryList.filter(({ code }) => code === userCountry || countriesCodesList.find(x => x === code));
},
);
export const preferredLanguageSelector = createSelector(
editableFormSelector,
sortedLanguagesSelector,
languageMessagesSelector,
(editableForm, sortedLanguages, languageMessages) => ({
...editableForm,
sortedLanguages,
languageMessages,
}),
);
export const countrySelector = createSelector(
editableFormSelector,
sortedCountriesSelector,
countryMessagesSelector,
countriesCodesListSelector,
profileAccountSelector,
(editableForm, translatedCountries, countryMessages, countriesCodesList, account) => ({
...editableForm,
translatedCountries,
countryMessages,
countriesCodesList,
committedCountry: account.country,
}),
);
export const certificatesSelector = createSelector(
editableFormSelector,
profileCourseCertificatesSelector,
(certificates) => ({
(editableForm, certificates) => ({
...editableForm,
certificates,
value: certificates,
}),
@@ -25,34 +148,191 @@ export const profileImageSelector = createSelector(
: {}),
);
export const handleSaveProfileSelector = createSelector(
profileDraftsSelector,
profilePreferencesSelector,
(drafts, preferences) => ({
drafts,
preferences,
}),
);
const socialLinksByPlatformSelector = createSelector(
profileAccountSelector,
(account) => {
const linksByPlatform = {};
if (Array.isArray(account.socialLinks)) {
account.socialLinks.forEach((socialLink) => {
linksByPlatform[socialLink.platform] = socialLink;
});
}
return linksByPlatform;
},
);
const draftSocialLinksByPlatformSelector = createSelector(
profileDraftsSelector,
(drafts) => {
const linksByPlatform = {};
if (Array.isArray(drafts.socialLinks)) {
drafts.socialLinks.forEach((socialLink) => {
linksByPlatform[socialLink.platform] = socialLink;
});
}
return linksByPlatform;
},
);
export const formSocialLinksSelector = createSelector(
socialLinksByPlatformSelector,
draftSocialLinksByPlatformSelector,
(linksByPlatform, draftLinksByPlatform) => {
const knownPlatforms = ['twitter', 'facebook', 'linkedin'];
const socialLinks = [];
knownPlatforms.forEach((platform) => {
if (draftLinksByPlatform[platform] !== undefined) {
socialLinks.push(draftLinksByPlatform[platform]);
} else if (linksByPlatform[platform] !== undefined) {
socialLinks.push(linksByPlatform[platform]);
} else {
socialLinks.push({
platform,
socialLink: null,
});
}
});
return socialLinks;
},
);
export const visibilitiesSelector = createSelector(
profilePreferencesSelector,
accountPrivacySelector,
(preferences, accountPrivacy) => {
switch (accountPrivacy) {
case 'custom':
return {
visibilityBio: preferences.visibilityBio || 'all_users',
visibilityCountry: preferences.visibilityCountry || 'all_users',
visibilityLevelOfEducation: preferences.visibilityLevelOfEducation || 'all_users',
visibilityLanguageProficiencies: preferences.visibilityLanguageProficiencies || 'all_users',
visibilityName: preferences.visibilityName || 'all_users',
visibilitySocialLinks: preferences.visibilitySocialLinks || 'all_users',
};
case 'private':
return {
visibilityBio: 'private',
visibilityCountry: 'private',
visibilityLevelOfEducation: 'private',
visibilityLanguageProficiencies: 'private',
visibilityName: 'private',
visibilitySocialLinks: 'private',
};
case 'all_users':
default:
return {
visibilityBio: 'all_users',
visibilityCountry: 'all_users',
visibilityLevelOfEducation: 'all_users',
visibilityLanguageProficiencies: 'all_users',
visibilityName: 'all_users',
visibilitySocialLinks: 'all_users',
};
}
},
);
function chooseFormValue(draft, committed) {
return draft !== undefined ? draft : committed;
}
export const formValuesSelector = createSelector(
profileAccountSelector,
visibilitiesSelector,
profileDraftsSelector,
profileCourseCertificatesSelector,
formSocialLinksSelector,
(account, visibilities, drafts, courseCertificates, socialLinks) => ({
bio: chooseFormValue(drafts.bio, account.bio),
visibilityBio: chooseFormValue(drafts.visibilityBio, visibilities.visibilityBio),
courseCertificates,
country: chooseFormValue(drafts.country, account.country),
visibilityCountry: chooseFormValue(drafts.visibilityCountry, visibilities.visibilityCountry),
levelOfEducation: chooseFormValue(drafts.levelOfEducation, account.levelOfEducation),
visibilityLevelOfEducation: chooseFormValue(
drafts.visibilityLevelOfEducation,
visibilities.visibilityLevelOfEducation,
),
languageProficiencies: chooseFormValue(
drafts.languageProficiencies,
account.languageProficiencies,
),
visibilityLanguageProficiencies: chooseFormValue(
drafts.visibilityLanguageProficiencies,
visibilities.visibilityLanguageProficiencies,
),
name: chooseFormValue(drafts.name, account.name),
visibilityName: chooseFormValue(drafts.visibilityName, visibilities.visibilityName),
socialLinks,
visibilitySocialLinks: chooseFormValue(
drafts.visibilitySocialLinks,
visibilities.visibilitySocialLinks,
),
}),
);
export const profilePageSelector = createSelector(
profileAccountSelector,
profileCourseCertificatesSelector,
formValuesSelector,
profileImageSelector,
saveStateSelector,
savePhotoStateSelector,
isLoadingProfileSelector,
draftSocialLinksByPlatformSelector,
accountErrorsSelector,
isAuthenticatedUserProfileSelector,
(
account,
courseCertificates,
formValues,
profileImage,
saveState,
savePhotoState,
isLoadingProfile,
draftSocialLinksByPlatform,
errors,
isAuthenticatedUserProfile,
) => ({
// Account data we need
username: account.username,
profileImage,
requiresParentalConsent: account.requiresParentalConsent,
dateJoined: account.dateJoined,
yearOfBirth: account.yearOfBirth,
name: account.name,
courseCertificates,
bio: formValues.bio,
visibilityBio: formValues.visibilityBio,
// Other data we need
courseCertificates: formValues.courseCertificates,
country: formValues.country,
visibilityCountry: formValues.visibilityCountry,
levelOfEducation: formValues.levelOfEducation,
visibilityLevelOfEducation: formValues.visibilityLevelOfEducation,
languageProficiencies: formValues.languageProficiencies,
visibilityLanguageProficiencies: formValues.visibilityLanguageProficiencies,
name: formValues.name,
visibilityName: formValues.visibilityName,
socialLinks: formValues.socialLinks,
visibilitySocialLinks: formValues.visibilitySocialLinks,
draftSocialLinksByPlatform,
saveState,
savePhotoState,
isLoadingProfile,
photoUploadError: errors.photo || null,
isAuthenticatedUserProfile,
}),
);

View File

@@ -2,11 +2,24 @@ import { ensureConfig, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient as getHttpClient } from '@edx/frontend-platform/auth';
import { logError } from '@edx/frontend-platform/logging';
import { camelCaseObject, convertKeyNames, snakeCaseObject } from '../utils';
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) {
@@ -19,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);
@@ -41,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(
@@ -70,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`);
@@ -85,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, {
@@ -114,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) => {
@@ -147,3 +148,21 @@ export async function getCourseCertificates(username) {
return [];
}
}
function extractCountryList(data) {
return data?.fields
.find(({ name }) => name === FIELD_LABELS.COUNTRY)
?.options?.map(({ value }) => (value)) || [];
}
export async function getCountryList() {
const url = `${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`;
try {
const { data } = await getHttpClient().get(url);
return extractCountryList(data);
} catch (e) {
logError(e);
return [];
}
}

View File

@@ -0,0 +1,151 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Form } from '@openedx/paragon';
import classNames from 'classnames';
import messages from './Bio.messages';
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
import { editableFormSelector } from '../data/selectors';
import {
useCloseOpenHandler,
useHandleChange,
useHandleSubmit,
useIsOnMobileScreen,
useIsVisibilityEnabled,
} from '../data/hooks';
const Bio = ({
formId,
bio,
visibilityBio,
editMode,
saveState,
error,
changeHandler,
submitHandler,
closeHandler,
openHandler,
}) => {
const isMobileView = useIsOnMobileScreen();
const isVisibilityEnabled = useIsVisibilityEnabled();
const intl = useIntl();
const handleChange = useHandleChange(changeHandler);
const handleSubmit = useHandleSubmit(submitHandler, formId);
const handleOpen = useCloseOpenHandler(openHandler, formId);
const handleClose = useCloseOpenHandler(closeHandler, formId);
return (
<SwitchContent
className={classNames([
isMobileView ? 'pt-40px' : 'pt-0',
])}
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={handleSubmit}>
<Form.Group
controlId={formId}
className="m-0 pb-3"
isInvalid={error !== null}
>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-2.5">
{intl.formatMessage(messages['profile.bio.about.me'])}
</p>
<textarea
className="form-control py-10px"
id={formId}
name={formId}
value={bio}
onChange={handleChange}
/>
{error !== null && (
<Form.Control.Feedback hasIcon={false}>
{error}
</Form.Control.Feedback>
)}
</Form.Group>
<FormControls
visibilityId="visibilityBio"
saveState={saveState}
visibility={visibilityBio}
cancelHandler={handleClose}
changeHandler={handleChange}
/>
</form>
</div>
),
editable: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.bio.about.me'])}
</p>
<EditableItemHeader
content={bio}
showEditButton
onClickEdit={handleOpen}
showVisibility={visibilityBio !== null && isVisibilityEnabled}
visibility={visibilityBio}
/>
</>
),
empty: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.bio.about.me'])}
</p>
<EmptyContent onClick={handleOpen}>
<FormattedMessage
id="profile.bio.empty"
defaultMessage="Add a short bio"
description="instructions when the user hasn't written an About Me"
/>
</EmptyContent>
</>
),
static: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.bio.about.me'])}
</p>
<EditableItemHeader content={bio} />
</>
),
}}
/>
);
};
Bio.propTypes = {
formId: PropTypes.string.isRequired,
bio: PropTypes.string,
visibilityBio: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
};
Bio.defaultProps = {
editMode: 'static',
saveState: null,
bio: null,
visibilityBio: 'private',
error: null,
};
export default connect(
editableFormSelector,
{},
)(Bio);

View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.bio.about.me': {
id: 'profile.bio.about.me',
defaultMessage: 'Bio',
description: 'A section of a user profile',
},
});
export default messages;

View File

@@ -0,0 +1,163 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form } from '@openedx/paragon';
import messages from './Country.messages';
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
import { countrySelector } from '../data/selectors';
import {
useCloseOpenHandler,
useHandleChange,
useHandleSubmit,
useIsVisibilityEnabled,
} from '../data/hooks';
const Country = ({
formId,
country,
visibilityCountry,
editMode,
saveState,
error,
translatedCountries,
countriesCodesList,
countryMessages,
changeHandler,
submitHandler,
closeHandler,
openHandler,
}) => {
const isVisibilityEnabled = useIsVisibilityEnabled();
const intl = useIntl();
const handleChange = useHandleChange(changeHandler);
const handleSubmit = useHandleSubmit(submitHandler, formId);
const handleOpen = useCloseOpenHandler(openHandler, formId);
const handleClose = useCloseOpenHandler(closeHandler, formId);
const isDisabledCountry = (countryCode) => countriesCodesList.length > 0
&& !countriesCodesList.find(code => code === countryCode);
return (
<SwitchContent
className="pt-40px"
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={handleSubmit}>
<Form.Group
controlId={formId}
className="m-0 pb-3"
isInvalid={error !== null}
>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-2.5">
{intl.formatMessage(messages['profile.country.label'])}
</p>
<select
data-hj-suppress
className="form-control py-10px"
type="select"
id={formId}
name={formId}
value={country}
onChange={handleChange}
>
<option value=""> </option>
{translatedCountries.map(({ code, name }) => (
<option key={code} value={code} disabled={isDisabledCountry(code)}>
{name}
</option>
))}
</select>
{error !== null && (
<Form.Control.Feedback hasIcon={false}>
{error}
</Form.Control.Feedback>
)}
</Form.Group>
<FormControls
visibilityId="visibilityCountry"
saveState={saveState}
visibility={visibilityCountry}
cancelHandler={handleClose}
changeHandler={handleChange}
/>
</form>
</div>
),
editable: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.country.label'])}
</p>
<EditableItemHeader
content={countryMessages[country]}
showEditButton
onClickEdit={handleOpen}
showVisibility={visibilityCountry !== null && isVisibilityEnabled}
visibility={visibilityCountry}
/>
</>
),
empty: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.country.label'])}
</p>
<EmptyContent onClick={handleOpen}>
{intl.formatMessage(messages['profile.country.empty'])}
</EmptyContent>
</>
),
static: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.country.label'])}
</p>
<EditableItemHeader content={countryMessages[country]} />
</>
),
}}
/>
);
};
Country.propTypes = {
formId: PropTypes.string.isRequired,
country: PropTypes.string,
visibilityCountry: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
translatedCountries: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})).isRequired,
countriesCodesList: PropTypes.arrayOf(PropTypes.string).isRequired,
countryMessages: PropTypes.objectOf(PropTypes.string).isRequired,
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
};
Country.defaultProps = {
editMode: 'static',
saveState: null,
country: null,
visibilityCountry: 'private',
error: null,
};
export default connect(
countrySelector,
{},
)(Country);

View File

@@ -0,0 +1,16 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.country.label': {
id: 'profile.country.label',
defaultMessage: 'Country',
description: 'The label for a country in a user profile.',
},
'profile.country.empty': {
id: 'profile.country.empty',
defaultMessage: 'Add country',
description: 'The affordance to add country location to a users profile.',
},
});
export default messages;

View File

@@ -0,0 +1,171 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import get from 'lodash.get';
import { Form } from '@openedx/paragon';
import messages from './Education.messages';
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
import { EDUCATION_LEVELS } from '../data/constants';
import { editableFormSelector } from '../data/selectors';
import {
useCloseOpenHandler,
useHandleChange,
useHandleSubmit,
useIsVisibilityEnabled,
} from '../data/hooks';
const Education = ({
formId,
levelOfEducation,
visibilityLevelOfEducation,
editMode,
saveState,
error,
changeHandler,
submitHandler,
closeHandler,
openHandler,
}) => {
const isVisibilityEnabled = useIsVisibilityEnabled();
const intl = useIntl();
const handleChange = useHandleChange(changeHandler);
const handleSubmit = useHandleSubmit(submitHandler, formId);
const handleOpen = useCloseOpenHandler(openHandler, formId);
const handleClose = useCloseOpenHandler(closeHandler, formId);
return (
<SwitchContent
className="pt-40px"
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={handleSubmit}>
<Form.Group
controlId={formId}
className="m-0 pb-3"
isInvalid={error !== null}
>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-2.5">
{intl.formatMessage(messages['profile.education.education'])}
</p>
<select
data-hj-suppress
className="form-control py-10px"
id={formId}
name={formId}
value={levelOfEducation}
onChange={handleChange}
>
<option value=""> </option>
{EDUCATION_LEVELS.map(level => (
<option key={level} value={level}>
{intl.formatMessage(get(
messages,
`profile.education.levels.${level}`,
messages['profile.education.levels.o'],
))}
</option>
))}
</select>
{error !== null && (
<Form.Control.Feedback hasIcon={false}>
{error}
</Form.Control.Feedback>
)}
</Form.Group>
<FormControls
visibilityId="visibilityLevelOfEducation"
saveState={saveState}
visibility={visibilityLevelOfEducation}
cancelHandler={handleClose}
changeHandler={handleChange}
/>
</form>
</div>
),
editable: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.education.education'])}
</p>
<EditableItemHeader
content={intl.formatMessage(get(
messages,
`profile.education.levels.${levelOfEducation}`,
messages['profile.education.levels.o'],
))}
showEditButton
onClickEdit={handleOpen}
showVisibility={visibilityLevelOfEducation !== null && isVisibilityEnabled}
visibility={visibilityLevelOfEducation}
/>
</>
),
empty: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.education.education'])}
</p>
<EmptyContent onClick={handleOpen}>
<FormattedMessage
id="profile.education.empty"
defaultMessage="Add level of education"
description="instructions when the user doesn't have their level of education set"
/>
</EmptyContent>
</>
),
static: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.education.education'])}
</p>
<EditableItemHeader
content={intl.formatMessage(get(
messages,
`profile.education.levels.${levelOfEducation}`,
messages['profile.education.levels.o'],
))}
/>
</>
),
}}
/>
);
};
Education.propTypes = {
formId: PropTypes.string.isRequired,
levelOfEducation: PropTypes.string,
visibilityLevelOfEducation: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
};
Education.defaultProps = {
editMode: 'static',
saveState: null,
levelOfEducation: null,
visibilityLevelOfEducation: 'private',
error: null,
};
export default connect(
editableFormSelector,
{},
)(Education);

View File

@@ -0,0 +1,56 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.education.education': {
id: 'profile.education.education',
defaultMessage: 'Education',
description: 'A section of a user profile',
},
'profile.education.levels.p': {
id: 'profile.education.levels.p',
defaultMessage: 'Doctorate',
description: 'Selected by the user if their highest level of education is a doctorate degree.',
},
'profile.education.levels.m': {
id: 'profile.education.levels.m',
defaultMessage: "Master's or professional degree",
description: "Selected by the user if their highest level of education is a master's or professional degree from a college or university.",
},
'profile.education.levels.b': {
id: 'profile.education.levels.b',
defaultMessage: "Bachelor's Degree",
description: "Selected by the user if their highest level of education is a four year college or university bachelor's degree.",
},
'profile.education.levels.a': {
id: 'profile.education.levels.a',
defaultMessage: "Associate's degree",
description: "Selected by the user if their highest level of education is an associate's degree. 1-2 years of college or university.",
},
'profile.education.levels.hs': {
id: 'profile.education.levels.hs',
defaultMessage: 'Secondary/high school',
description: 'Selected by the user if their highest level of education is secondary or high school. 9-12 years of education.',
},
'profile.education.levels.jhs': {
id: 'profile.education.levels.jhs',
defaultMessage: 'Junior secondary/junior high/middle school',
description: 'Selected by the user if their highest level of education is junior or middle school. 6-8 years of education.',
},
'profile.education.levels.el': {
id: 'profile.education.levels.el',
defaultMessage: 'Elementary/primary school',
description: 'Selected by the user if their highest level of education is elementary or primary school. 1-5 years of education.',
},
'profile.education.levels.none': {
id: 'profile.education.levels.none',
defaultMessage: 'No formal education',
description: 'Selected by the user to describe their education.',
},
'profile.education.levels.o': {
id: 'profile.education.levels.o',
defaultMessage: 'Other education',
description: 'Selected by the user if they have a type of education not described by the other choices.',
},
});
export default messages;

View File

@@ -0,0 +1,192 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
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';
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
import { editableFormSelector } from '../data/selectors';
import {
useCloseOpenHandler,
useHandleChange,
useHandleSubmit,
useIsVisibilityEnabled,
} from '../data/hooks';
const Name = ({
formId,
name,
visibilityName,
editMode,
saveState,
changeHandler,
submitHandler,
closeHandler,
openHandler,
accountSettingsUrl,
}) => {
const isVisibilityEnabled = useIsVisibilityEnabled();
const intl = useIntl();
const handleChange = useHandleChange(changeHandler);
const handleSubmit = useHandleSubmit(submitHandler, formId);
const handleOpen = useCloseOpenHandler(openHandler, formId);
const handleClose = useCloseOpenHandler(closeHandler, formId);
return (
<SwitchContent
className="pt-40px"
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={handleSubmit}>
<div className="form-group">
<div className="row m-0 pb-2.5 align-items-center">
<p data-hj-suppress className="h5 font-weight-bold m-0">
{intl.formatMessage(messages['profile.name.full.name'])}
</p>
<OverlayTrigger
key="top"
placement="top"
overlay={(
<Tooltip variant="light" id="tooltip-top">
<p className="h5 font-weight-normal m-0 p-0">
{intl.formatMessage(messages['profile.name.tooltip'])}
</p>
</Tooltip>
)}
>
<InfoOutline className="m-0 info-icon" />
</OverlayTrigger>
</div>
<EditableItemHeader content={name} />
<h4 className="font-weight-normal">
<Hyperlink destination={accountSettingsUrl} target="_blank">
{intl.formatMessage(messages['profile.name.redirect'])}
</Hyperlink>
</h4>
</div>
<FormControls
visibilityId="visibilityName"
saveState={saveState}
visibility={visibilityName}
cancelHandler={handleClose}
changeHandler={handleChange}
/>
</form>
</div>
),
editable: (
<>
<div className="row m-0 pb-1.5 align-items-center">
<p data-hj-suppress className="h5 font-weight-bold m-0">
{intl.formatMessage(messages['profile.name.full.name'])}
</p>
<OverlayTrigger
key="top"
placement="top"
overlay={(
<Tooltip variant="light" id="tooltip-top">
<p className="h5 font-weight-normal m-0 p-0">
{intl.formatMessage(messages['profile.name.tooltip'])}
</p>
</Tooltip>
)}
>
<InfoOutline className="m-0 info-icon" />
</OverlayTrigger>
</div>
<EditableItemHeader
content={name}
showEditButton
onClickEdit={handleOpen}
showVisibility={visibilityName !== null && isVisibilityEnabled}
visibility={visibilityName}
/>
</>
),
empty: (
<>
<div className="row m-0 pb-1.5 align-items-center">
<p data-hj-suppress className="h5 font-weight-bold m-0">
{intl.formatMessage(messages['profile.name.full.name'])}
</p>
<OverlayTrigger
key="top"
placement="top"
overlay={(
<Tooltip variant="light" id="tooltip-top">
<p className="h5 font-weight-normal m-0 p-0">
{intl.formatMessage(messages['profile.name.tooltip'])}
</p>
</Tooltip>
)}
>
<InfoOutline className="m-0 info-icon" />
</OverlayTrigger>
</div>
<EmptyContent onClick={handleOpen}>
{intl.formatMessage(messages['profile.name.empty'])}
</EmptyContent>
</>
),
static: (
<>
<div className="row m-0 pb-1.5 align-items-center">
<p data-hj-suppress className="h5 font-weight-bold m-0">
{intl.formatMessage(messages['profile.name.full.name'])}
</p>
<OverlayTrigger
key="top"
placement="top"
overlay={(
<Tooltip variant="light" id="tooltip-top">
<p className="h5 font-weight-normal m-0 p-0">
{intl.formatMessage(messages['profile.name.tooltip'])}
</p>
</Tooltip>
)}
>
<InfoOutline className="m-0 info-icon" />
</OverlayTrigger>
</div>
<EditableItemHeader content={name} />
</>
),
}}
/>
);
};
Name.propTypes = {
formId: PropTypes.string.isRequired,
name: PropTypes.string,
visibilityName: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
accountSettingsUrl: PropTypes.string.isRequired,
};
Name.defaultProps = {
editMode: 'static',
saveState: null,
name: null,
visibilityName: 'private',
};
export default connect(
editableFormSelector,
{},
)(Name);

View File

@@ -0,0 +1,26 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.name.full.name': {
id: 'profile.name.full.name',
defaultMessage: 'Full name',
description: 'A section of a user profile',
},
'profile.name.empty': {
id: 'profile.name.empty',
defaultMessage: 'Add full name',
description: 'The affordance to add a name to a users 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;

View File

@@ -0,0 +1,166 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form } from '@openedx/paragon';
import messages from './PreferredLanguage.messages';
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
import { preferredLanguageSelector } from '../data/selectors';
import {
useCloseOpenHandler,
useHandleSubmit,
useIsVisibilityEnabled,
} from '../data/hooks';
const PreferredLanguage = ({
formId,
languageProficiencies,
visibilityLanguageProficiencies,
editMode,
saveState,
error,
sortedLanguages,
languageMessages,
changeHandler,
submitHandler,
closeHandler,
openHandler,
}) => {
const isVisibilityEnabled = useIsVisibilityEnabled();
const intl = useIntl();
const handleChange = ({ target: { name, value } }) => {
let newValue = value;
if (name === formId) {
newValue = value ? [{ code: value }] : [];
}
changeHandler(name, newValue);
};
const handleSubmit = useHandleSubmit(submitHandler, formId);
const handleOpen = useCloseOpenHandler(openHandler, formId);
const handleClose = useCloseOpenHandler(closeHandler, formId);
const value = languageProficiencies.length ? languageProficiencies[0].code : '';
return (
<SwitchContent
className="pt-40px"
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={handleSubmit}>
<Form.Group
controlId={formId}
className="m-0 pb-3"
isInvalid={error !== null}
>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-2.5">
{intl.formatMessage(messages['profile.preferredlanguage.label'])}
</p>
<select
data-hj-suppress
id={formId}
name={formId}
className="form-control py-10px"
value={value}
onChange={handleChange}
>
<option value=""> </option>
{sortedLanguages.map(({ code, name }) => (
<option key={code} value={code}>{name}</option>
))}
</select>
{error !== null && (
<Form.Control.Feedback hasIcon={false}>
{error}
</Form.Control.Feedback>
)}
</Form.Group>
<FormControls
visibilityId="visibilityLanguageProficiencies"
saveState={saveState}
visibility={visibilityLanguageProficiencies}
cancelHandler={handleClose}
changeHandler={handleChange}
/>
</form>
</div>
),
editable: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.preferredlanguage.label'])}
</p>
<EditableItemHeader
content={languageMessages[value]}
showEditButton
onClickEdit={handleOpen}
showVisibility={visibilityLanguageProficiencies !== null && isVisibilityEnabled}
visibility={visibilityLanguageProficiencies}
/>
</>
),
empty: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.preferredlanguage.label'])}
</p>
<EmptyContent onClick={handleOpen}>
{intl.formatMessage(messages['profile.preferredlanguage.empty'])}
</EmptyContent>
</>
),
static: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.preferredlanguage.label'])}
</p>
<EditableItemHeader content={languageMessages[value]} />
</>
),
}}
/>
);
};
PreferredLanguage.propTypes = {
formId: PropTypes.string.isRequired,
languageProficiencies: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.shape({ code: PropTypes.string })),
PropTypes.oneOf(['']),
]),
visibilityLanguageProficiencies: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
sortedLanguages: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})).isRequired,
languageMessages: PropTypes.objectOf(PropTypes.string).isRequired,
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
};
PreferredLanguage.defaultProps = {
editMode: 'static',
saveState: null,
languageProficiencies: [],
visibilityLanguageProficiencies: 'private',
error: null,
};
export default connect(
preferredLanguageSelector,
{},
)(PreferredLanguage);

View File

@@ -0,0 +1,16 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.preferredlanguage.empty': {
id: 'profile.preferredlanguage.empty',
defaultMessage: 'Add language',
description: 'Instructions when the user doesnt have a preferred language set.',
},
'profile.preferredlanguage.label': {
id: 'profile.preferredlanguage.label',
defaultMessage: 'Primary language spoken',
description: 'The label for a users primary spoken language.',
},
});
export default messages;

View File

@@ -1,8 +1,15 @@
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import { Button, Dropdown } from '@openedx/paragon';
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';
@@ -40,63 +47,67 @@ const ProfileAvatar = ({
const renderPending = () => (
<div
className="position-absolute w-100 h-100 d-flex justify-content-center
align-items-center rounded-circle background-black-65"
className="position-absolute w-100 h-100 d-flex justify-content-center align-items-center rounded-circle bg-black bg-opacity-65"
>
<div className="spinner-border text-primary" role="status" />
</div>
);
const renderMenuContent = () => {
if (isDefault) {
return (
<Button
variant="link"
size="sm"
className="text-white btn-block"
onClick={onClickUpload}
>
<FormattedMessage
id="profile.profileavatar.upload-button"
defaultMessage="Upload Photo"
description="Upload photo button"
/>
</Button>
);
}
return (
<Dropdown>
<Dropdown.Toggle>
{intl.formatMessage(messages['profile.profileavatar.change-button'])}
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item type="button" onClick={onClickUpload}>
<FormattedMessage
id="profile.profileavatar.upload-button"
defaultMessage="Upload Photo"
description="Upload photo button"
/>
</Dropdown.Item>
<Dropdown.Item type="button" onClick={onClickDelete}>
<FormattedMessage
id="profile.profileavatar.remove.button"
defaultMessage="Remove"
description="Remove photo button"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);
};
const renderMenu = () => {
const renderEditButton = () => {
if (!isEditable) {
return null;
}
return (
<div className="profile-avatar-menu-container">
{renderMenuContent()}
<div className="profile-avatar-button">
<Dropdown>
<OverlayTrigger
key="top"
placement="top"
overlay={(
<Tooltip variant="light" id="tooltip-top">
{!isDefault ? (
<p className="h5 font-weight-normal m-0 p-0">
{intl.formatMessage(messages['profile.profileavatar.tooltip.edit'])}
</p>
) : (
<p className="h5 font-weight-normal m-0 p-0">
{intl.formatMessage(messages['profile.profileavatar.tooltip.upload'])}
</p>
)}
</Tooltip>
)}
>
<Dropdown.Toggle
invertColors
isActive
id="dropdown-toggle-with-iconbutton"
as={IconButton}
src={PhotoCamera}
iconAs={Icon}
variant="primary"
className="shadow-sm"
/>
</OverlayTrigger>
<Dropdown.Menu className="min-width-179px p-0 m-0">
<Dropdown.Item type="button" onClick={onClickUpload}>
<FormattedMessage
id="profile.profileavatar.upload-button"
defaultMessage="Upload photo"
description="Upload photo button"
/>
</Dropdown.Item>
{!isDefault && (
<Dropdown.Item type="button" onClick={onClickDelete}>
<FormattedMessage
id="profile.profileavatar.remove.button"
defaultMessage="Remove photo"
description="Remove photo button"
/>
</Dropdown.Item>
)}
</Dropdown.Menu>
</Dropdown>
</div>
);
};
@@ -117,15 +128,15 @@ const ProfileAvatar = ({
return (
<div className="profile-avatar-wrap position-relative">
<div className="profile-avatar rounded-circle bg-light">
{savePhotoState === 'pending' ? renderPending() : renderMenu()}
{savePhotoState === 'pending' && renderPending()}
{renderAvatar()}
</div>
{renderEditButton()}
<form
ref={form}
onSubmit={onSubmit}
encType="multipart/form-data"
>
{/* The name of this input must be 'file' */}
<input
className="d-none form-control-file"
ref={fileInput}

View File

@@ -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;

View File

@@ -0,0 +1,258 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Alert } from '@openedx/paragon';
import { connect } from 'react-redux';
import { faTwitter, faFacebook, faLinkedin } from '@fortawesome/free-brands-svg-icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import classNames from 'classnames';
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
import { editableFormSelector } from '../data/selectors';
import { useIsVisibilityEnabled } from '../data/hooks';
const platformDisplayInfo = {
facebook: {
icon: faFacebook,
name: 'Facebook',
},
twitter: {
icon: faTwitter,
name: 'X',
},
linkedin: {
icon: faLinkedin,
name: 'LinkedIn',
},
};
const SocialLinks = ({
formId,
socialLinks,
draftSocialLinksByPlatform,
visibilitySocialLinks,
editMode,
saveState,
error,
changeHandler,
submitHandler,
closeHandler,
openHandler,
}) => {
const isVisibilityEnabled = useIsVisibilityEnabled();
const [activePlatform, setActivePlatform] = useState(null);
const mergeWithDrafts = (newSocialLink) => {
const knownPlatforms = ['twitter', 'facebook', 'linkedin'];
const updated = [];
knownPlatforms.forEach((platform) => {
if (newSocialLink.platform === platform) {
updated.push(newSocialLink);
} else if (draftSocialLinksByPlatform[platform] !== undefined) {
updated.push(draftSocialLinksByPlatform[platform]);
}
});
return updated;
};
const handleChange = (e) => {
const { name, value } = e.target;
if (name !== 'visibilitySocialLinks') {
changeHandler(
'socialLinks',
mergeWithDrafts({
platform: name,
socialLink: value,
}),
);
} else {
changeHandler(name, value);
}
};
const handleSubmit = (e) => {
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 (
<form onSubmit={handleSubmit}>
<div className="form-group m-0">
{error !== null && (
<div id="social-error-feedback">
<Alert variant="danger" dismissible={false} show>
{error}
</Alert>
</div>
)}
<div className="pb-3">
<input
className={classNames('form-control py-10px', { 'is-invalid': Boolean(error) })}
type="text"
id={`social-${platform}`}
name={platform}
value={socialLink || ''}
onChange={handleChange}
aria-describedby="social-error-feedback"
/>
</div>
<FormControls
visibilityId="visibilitySocialLinks"
saveState={saveState}
visibility={visibilitySocialLinks}
cancelHandler={handleClose}
changeHandler={handleChange}
submitHandler={handleSubmit}
/>
</div>
</form>
);
}
if (socialLink) {
return (
<div className="w-100 overflowWrap-breakWord">
<EditableItemHeader
content={socialLink}
showEditButton
onClickEdit={() => handleOpen(platform)}
showVisibility={visibilitySocialLinks !== null && isVisibilityEnabled}
visibility={visibilitySocialLinks}
/>
</div>
);
}
return (
<EmptyContent onClick={() => handleOpen(platform)}>
Add {platformDisplayInfo[platform].name}
</EmptyContent>
);
};
return (
<SwitchContent
className="p-0"
expression={editMode}
cases={{
empty: (
<div>
<div>
{socialLinks.map(({ platform }) => (
<div key={platform} className="pt-40px">
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{platformDisplayInfo[platform].name}
</p>
<EmptyContent onClick={() => handleOpen(platform)}>
<FormattedMessage
id="profile.sociallinks.add"
defaultMessage="Add {network} profile"
values={{
network: platformDisplayInfo[platform].name,
}}
description="{network} is the name of a social network such as Facebook or Twitter"
/>
</EmptyContent>
</div>
))}
</div>
</div>
),
static: (
<div>
<div>
{socialLinks
.filter(({ socialLink }) => Boolean(socialLink))
.map(({ platform, socialLink }) => (
<div key={platform} className="pt-40px">
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{platformDisplayInfo[platform].name}
</p>
<EditableItemHeader
content={socialLink}
contentPrefix={`${platformDisplayInfo[platform].name}: `}
/>
</div>
))}
</div>
</div>
),
editable: (
<div>
<div>
{socialLinks.map(({ platform, socialLink }) => (
<div key={platform} className="pt-40px">
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{platformDisplayInfo[platform].name}
</p>
{renderPlatformContent(platform, socialLink, activePlatform === platform)}
</div>
))}
</div>
</div>
),
editing: (
<div>
<div>
{socialLinks.map(({ platform, socialLink }) => (
<div key={platform} className="pt-40px">
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-2.5">
{platformDisplayInfo[platform].name}
</p>
{renderPlatformContent(platform, socialLink, activePlatform === platform)}
</div>
))}
</div>
</div>
),
}}
/>
);
};
SocialLinks.propTypes = {
formId: PropTypes.string.isRequired,
socialLinks: PropTypes.arrayOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
})).isRequired,
draftSocialLinksByPlatform: PropTypes.objectOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
})),
visibilitySocialLinks: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
};
SocialLinks.defaultProps = {
editMode: 'static',
saveState: null,
draftSocialLinksByPlatform: {},
visibilitySocialLinks: 'private',
error: null,
};
export default connect(
editableFormSelector,
{},
)(SocialLinks);

View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.sociallinks.social.links': {
id: 'profile.sociallinks.social.links',
defaultMessage: 'Social Links',
description: 'A section of a user profile',
},
});
export default messages;

View File

@@ -0,0 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EditOutline } from '@openedx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, OverlayTrigger, Tooltip } from '@openedx/paragon';
import messages from './EditButton.messages';
const EditButton = ({
onClick, className, style, intl,
}) => (
<OverlayTrigger
key="top"
placement="top"
overlay={(
<Tooltip variant="light" id="tooltip-top">
<p className="h5 font-weight-normal m-0 p-0">
{intl.formatMessage(messages['profile.editbutton.edit'])}
</p>
</Tooltip>
)}
>
<Button
variant="link"
size="sm"
className={className}
onClick={onClick}
style={style}
>
<EditOutline className="text-gray-700" />
</Button>
</OverlayTrigger>
);
export default injectIntl(EditButton);
EditButton.propTypes = {
onClick: PropTypes.func.isRequired,
className: PropTypes.string,
style: PropTypes.object, // eslint-disable-line
intl: intlShape.isRequired,
};
EditButton.defaultProps = {
className: null,
style: null,
};

View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.editbutton.edit': {
id: 'profile.editbutton.edit',
defaultMessage: 'Edit',
description: 'A button label',
},
});
export default messages;

View File

@@ -0,0 +1,69 @@
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,
showVisibility,
visibility,
showEditButton,
onClickEdit,
headingId,
}) => {
const isMobileView = useIsOnMobileScreen();
return (
<>
<div className="row m-0 p-0 d-flex flex-nowrap align-items-center">
<div
className={classNames([
'm-0 p-0 col-auto',
isMobileView ? 'w-90' : '',
])}
>
<h4
className="edit-section-header text-gray-700"
id={headingId}
>
{content}
</h4>
</div>
<div
className={classNames([
'col-auto m-0 p-0 d-flex align-items-center',
isMobileView ? 'col-1' : 'col-auto',
])}
>
{showEditButton ? <EditButton className="p-1.5" onClick={onClickEdit} /> : null}
</div>
</div>
<div className="row m-0 p-0">
{showVisibility ? <p className="mb-0"><Visibility to={visibility} /></p> : null}
</div>
</>
);
};
export default EditableItemHeader;
EditableItemHeader.propTypes = {
onClickEdit: PropTypes.func,
showVisibility: PropTypes.bool,
showEditButton: PropTypes.bool,
content: PropTypes.node,
visibility: PropTypes.oneOf(['private', 'all_users']),
headingId: PropTypes.string,
};
EditableItemHeader.defaultProps = {
onClickEdit: () => {
},
showVisibility: false,
showEditButton: false,
content: '',
visibility: 'private',
headingId: null,
};

View File

@@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
const EmptyContent = ({ children, onClick, showPlusIcon }) => (
<div className="p-0 m-0">
{onClick ? (
<button
type="button"
className="p-0 text-left btn btn-link lh-36px"
onClick={onClick}
onKeyDown={(e) => { if (e.key === 'Enter') { onClick(); } }}
tabIndex={0}
>
{showPlusIcon ? <FontAwesomeIcon size="xs" className="mr-1" icon={faPlus} /> : null}
{children}
</button>
) : children}
</div>
);
export default EmptyContent;
EmptyContent.propTypes = {
onClick: PropTypes.func,
children: PropTypes.node,
showPlusIcon: PropTypes.bool,
};
EmptyContent.defaultProps = {
onClick: null,
children: null,
showPlusIcon: true,
};

View File

@@ -0,0 +1,84 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, StatefulButton } from '@openedx/paragon';
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,
}) => {
const buttonState = saveState === 'error' ? null : saveState;
const isVisibilityEnabled = useIsVisibilityEnabled();
return (
<div className="d-flex flex-row-reverse flex-wrap justify-content-end align-items-center">
{isVisibilityEnabled && (
<div className="form-group d-flex flex-wrap">
<label className="col-form-label" htmlFor={visibilityId}>
{intl.formatMessage(messages['profile.formcontrols.who.can.see'])}
</label>
<VisibilitySelect
id={visibilityId}
className="d-flex align-items-center"
type="select"
name={visibilityId}
value={visibility}
onChange={changeHandler}
/>
</div>
)}
<div className="row form-group flex-shrink-0 flex-grow-1 m-0 p-0">
<div className="pr-2 pl-0 m-0">
<Button variant="outline-primary" onClick={cancelHandler}>
{intl.formatMessage(messages['profile.formcontrols.button.cancel'])}
</Button>
</div>
<div className="p-0 m-0">
<StatefulButton
type="submit"
state={buttonState}
labels={{
default: intl.formatMessage(messages['profile.formcontrols.button.save']),
pending: intl.formatMessage(messages['profile.formcontrols.button.saving']),
complete: intl.formatMessage(messages['profile.formcontrols.button.saved']),
}}
onClick={(e) => {
// 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).
// Disabling it would causes upstream issues in focus management.
// 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={[]}
/>
</div>
</div>
</div>
);
};
export default injectIntl(FormControls);
FormControls.propTypes = {
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
visibility: PropTypes.oneOf(['private', 'all_users']),
visibilityId: PropTypes.string.isRequired,
cancelHandler: PropTypes.func.isRequired,
changeHandler: PropTypes.func.isRequired,
intl: intlShape.isRequired,
};
FormControls.defaultProps = {
visibility: 'private',
saveState: null,
};

View File

@@ -0,0 +1,31 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.formcontrols.who.can.see': {
id: 'profile.formcontrols.who.can.see',
defaultMessage: 'Who can see this:',
description: 'What users can see this area?',
},
'profile.formcontrols.button.cancel': {
id: 'profile.formcontrols.button.cancel',
defaultMessage: 'Cancel',
description: 'A button label',
},
'profile.formcontrols.button.save': {
id: 'profile.formcontrols.button.save',
defaultMessage: 'Save',
description: 'A button label',
},
'profile.formcontrols.button.saving': {
id: 'profile.formcontrols.button.saving',
defaultMessage: 'Saving',
description: 'A button label',
},
'profile.formcontrols.button.saved': {
id: 'profile.formcontrols.button.saved',
defaultMessage: 'Saved',
description: 'A button label',
},
});
export default messages;

View File

@@ -0,0 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TransitionReplace } from '@openedx/paragon';
const onChildExit = (htmlNode) => {
if (htmlNode.contains(document.activeElement)) {
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
if (!enteringChild) {
return;
}
const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusableElements.length) {
focusableElements[0].focus();
}
}
};
const SwitchContent = ({ expression, cases, className }) => {
const getContent = (caseKey) => {
if (cases[caseKey]) {
if (typeof cases[caseKey] === 'string') {
return getContent(cases[caseKey]);
}
return React.cloneElement(cases[caseKey], { key: caseKey });
}
if (cases.default) {
if (typeof cases.default === 'string') {
return getContent(cases.default);
}
React.cloneElement(cases.default, { key: 'default' });
}
return null;
};
return (
<TransitionReplace
className={className}
onChildExit={onChildExit}
>
{getContent(expression)}
</TransitionReplace>
);
};
SwitchContent.propTypes = {
expression: PropTypes.string,
cases: PropTypes.objectOf(PropTypes.node).isRequired,
className: PropTypes.string,
};
SwitchContent.defaultProps = {
expression: null,
className: null,
};
export default SwitchContent;

View File

@@ -0,0 +1,76 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEyeSlash, faEye } from '@fortawesome/free-regular-svg-icons';
import messages from './Visibility.messages';
const Visibility = ({ to, intl }) => {
const icon = to === 'private' ? faEyeSlash : faEye;
const label = to === 'private'
? intl.formatMessage(messages['profile.visibility.who.just.me'])
: intl.formatMessage(messages['profile.visibility.who.everyone'], { siteName: getConfig().SITE_NAME });
return (
<span className="ml-auto small text-muted">
<FontAwesomeIcon icon={icon} /> {label}
</span>
);
};
Visibility.propTypes = {
to: PropTypes.oneOf(['private', 'all_users']),
intl: intlShape.isRequired,
};
Visibility.defaultProps = {
to: 'private',
};
const VisibilitySelect = ({ intl, className, ...props }) => {
const { value } = props;
const icon = value === 'private' ? faEyeSlash : faEye;
return (
<span className={className}>
<span className="d-inline-block ml-1 mr-2 width-24px">
<FontAwesomeIcon icon={icon} />
</span>
<select className="d-inline-block form-control" {...props}>
<option key="private" value="private">
{intl.formatMessage(messages['profile.visibility.who.just.me'])}
</option>
<option key="all_users" value="all_users">
{intl.formatMessage(messages['profile.visibility.who.everyone'], { siteName: getConfig().SITE_NAME })}
</option>
</select>
</span>
);
};
VisibilitySelect.propTypes = {
id: PropTypes.string,
className: PropTypes.string,
name: PropTypes.string,
value: PropTypes.oneOf(['private', 'all_users']),
onChange: PropTypes.func,
intl: intlShape.isRequired,
};
VisibilitySelect.defaultProps = {
id: null,
className: null,
name: 'visibility',
value: null,
onChange: null,
};
const intlVisibility = injectIntl(Visibility);
const intlVisibilitySelect = injectIntl(VisibilitySelect);
export {
intlVisibility as Visibility,
intlVisibilitySelect as VisibilitySelect,
};

View File

@@ -0,0 +1,16 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.visibility.who.just.me': {
id: 'profile.visibility.who.just.me',
defaultMessage: 'Just me',
description: 'What users can see this area?',
},
'profile.visibility.who.everyone': {
id: 'profile.visibility.who.everyone',
defaultMessage: 'Everyone on {siteName}',
description: 'What users can see this area?',
},
});
export default messages;

View File

@@ -31,11 +31,12 @@
.profile-page {
.edit-section-header {
@extend .h6;
@extend .h4;
display: block;
font-weight: normal;
font-weight: 400;
letter-spacing: 0;
margin: 0;
line-height: 2.25rem;
}
label.edit-section-header {
@@ -50,6 +51,12 @@
}
}
.profile-avatar-button {
position: absolute;
left: 76px;
top: 76px;
}
.profile-avatar-menu-container {
background: rgba(0,0,0,.65);
position: absolute;
@@ -87,8 +94,8 @@
}
.profile-avatar {
width: 5rem;
height: 5rem;
width: 7.5rem;
height: 7.5rem;
position: relative;
@include media-breakpoint-up(md) {
@@ -141,65 +148,80 @@
}
}
// Todo: Move the following to edx-paragon
.btn-rounded {
border-radius: 100px;
.info-icon {
width: 1.5rem;
height: 1.5rem;
padding-left: 0.125rem;
}
.max-width-32em {
max-width: 32em;
}
.max-width-19rem{
max-width: 19rem;
}
.width-75rem {
width: 75rem;
}
.width-72rem {
width: 72rem !important;
}
.width-19625rem {
width: 19.625rem;
}
.height-2625rem {
height: 2.625rem;
}
.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-4rem {
padding-top: 4rem;
.pt-40px{
padding-top: 40px;
}
.py-4rem {
padding-top: 4rem;
padding-bottom: 4rem;
.pl-40px {
padding-left: 40px;
}
.py-0625rem {
padding-top: 0.625rem;
padding-bottom: 0.625rem;
.py-10px{
padding-top: 10px;
padding-bottom: 10px;
}
.px-75rem {
padding-left: 7.5rem;
padding-right: 7.5rem;
.py-36px {
padding-top: 36px;
padding-bottom: 36px;
}
.px-25rem {
padding-left: 2.5rem;
padding-right: 2.5rem;
.px-120px {
padding-left: 120px;
padding-right: 120px;
}
.px-40px {
padding-left: 40px;
padding-right: 40px;
}
.g-15rem {
@@ -222,6 +244,10 @@
color: #000;
}
.bg-color-grey-FBFAF9 {
background-color: #FBFAF9;
}
.background-black-65 {
background-color: rgba(0,0,0,.65)
}
@@ -229,3 +255,11 @@
.object-fit-cover {
object-fit: cover;
}
.lh-36px {
line-height: 36px;
}
.overflowWrap-breakWord {
overflow-wrap: break-word;
}

View File

@@ -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);

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import NotFoundPage from './NotFoundPage';
describe('NotFoundPage Snapshot Tests', () => {
it('renders correctly', () => {
const { asFragment } = render(
<IntlProvider locale="en">
<NotFoundPage />
</IntlProvider>,
);
expect(asFragment()).toMatchSnapshot();
});
it('renders with custom props', () => {
const { asFragment } = render(
<IntlProvider locale="en">
<NotFoundPage message="Custom not found message" />
</IntlProvider>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@@ -29,6 +29,7 @@ module.exports = {
drafts: {},
isLoadingProfile: false,
isAuthenticatedUserProfile: true,
countriesCodesList: [],
},
router: {
location: {

View File

@@ -126,7 +126,7 @@ module.exports = {
],
drafts: {},
isLoadingProfile: false,
disabledCountries: [],
countriesCodesList: [],
},
router: {
location: {

View File

@@ -86,6 +86,7 @@ module.exports = {
drafts: {},
isLoadingProfile: false,
learningGoal: 'advance_career',
countriesCodesList: [],
},
router: {
location: {

View File

@@ -124,6 +124,7 @@ module.exports = {
createdDate: '2019-03-04T19:31:39.896806Z'
}
],
countriesCodesList:[{code:"AX"},{code:"AL"}],
drafts: {},
isLoadingProfile: false
},

View File

@@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotFoundPage Snapshot Tests renders correctly 1`] = `
<DocumentFragment>
<div
class="container-fluid d-flex py-5 justify-content-center align-items-start text-center"
>
<p
class="my-0 py-5 text-muted"
style="max-width: 32em;"
>
The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.
</p>
</div>
</DocumentFragment>
`;
exports[`NotFoundPage Snapshot Tests renders with custom props 1`] = `
<DocumentFragment>
<div
class="container-fluid d-flex py-5 justify-content-center align-items-start text-center"
>
<p
class="my-0 py-5 text-muted"
style="max-width: 32em;"
>
The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.
</p>
</div>
</DocumentFragment>
`;

View File

@@ -977,1256 +977,11 @@ exports[`<ProfilePage /> Renders correctly in various states test country edit w
>
 
</option>
<option
value="AF"
>
Afghanistan
</option>
<option
value="AL"
>
Albania
</option>
<option
value="DZ"
>
Algeria
</option>
<option
value="AS"
>
American Samoa
</option>
<option
value="AD"
>
Andorra
</option>
<option
value="AO"
>
Angola
</option>
<option
value="AI"
>
Anguilla
</option>
<option
value="AQ"
>
Antarctica
</option>
<option
value="AG"
>
Antigua and Barbuda
</option>
<option
value="AR"
>
Argentina
</option>
<option
value="AM"
>
Armenia
</option>
<option
value="AW"
>
Aruba
</option>
<option
value="AU"
>
Australia
</option>
<option
value="AT"
>
Austria
</option>
<option
value="AZ"
>
Azerbaijan
</option>
<option
value="BS"
>
Bahamas
</option>
<option
value="BH"
>
Bahrain
</option>
<option
value="BD"
>
Bangladesh
</option>
<option
value="BB"
>
Barbados
</option>
<option
value="BY"
>
Belarus
</option>
<option
value="BE"
>
Belgium
</option>
<option
value="BZ"
>
Belize
</option>
<option
value="BJ"
>
Benin
</option>
<option
value="BM"
>
Bermuda
</option>
<option
value="BT"
>
Bhutan
</option>
<option
value="BO"
>
Bolivia
</option>
<option
value="BA"
>
Bosnia and Herzegovina
</option>
<option
value="BW"
>
Botswana
</option>
<option
value="BV"
>
Bouvet Island
</option>
<option
value="BR"
>
Brazil
</option>
<option
value="IO"
>
British Indian Ocean Territory
</option>
<option
value="BN"
>
Brunei Darussalam
</option>
<option
value="BG"
>
Bulgaria
</option>
<option
value="BF"
>
Burkina Faso
</option>
<option
value="BI"
>
Burundi
</option>
<option
value="KH"
>
Cambodia
</option>
<option
value="CM"
>
Cameroon
</option>
<option
value="CA"
>
Canada
</option>
<option
value="CV"
>
Cape Verde
</option>
<option
value="KY"
>
Cayman Islands
</option>
<option
value="CF"
>
Central African Republic
</option>
<option
value="TD"
>
Chad
</option>
<option
value="CL"
>
Chile
</option>
<option
value="CN"
>
China
</option>
<option
value="CX"
>
Christmas Island
</option>
<option
value="CC"
>
Cocos (Keeling) Islands
</option>
<option
value="CO"
>
Colombia
</option>
<option
value="KM"
>
Comoros
</option>
<option
value="CG"
>
Congo
</option>
<option
value="CD"
>
Congo, the Democratic Republic of the
</option>
<option
value="CK"
>
Cook Islands
</option>
<option
value="CR"
>
Costa Rica
</option>
<option
value="CI"
>
Cote D'Ivoire
</option>
<option
value="HR"
>
Croatia
</option>
<option
value="CU"
>
Cuba
</option>
<option
value="CY"
>
Cyprus
</option>
<option
value="CZ"
>
Czech Republic
</option>
<option
value="DK"
>
Denmark
</option>
<option
value="DJ"
>
Djibouti
</option>
<option
value="DM"
>
Dominica
</option>
<option
value="DO"
>
Dominican Republic
</option>
<option
value="EC"
>
Ecuador
</option>
<option
value="EG"
>
Egypt
</option>
<option
value="SV"
>
El Salvador
</option>
<option
value="GQ"
>
Equatorial Guinea
</option>
<option
value="ER"
>
Eritrea
</option>
<option
value="EE"
>
Estonia
</option>
<option
value="ET"
>
Ethiopia
</option>
<option
value="FK"
>
Falkland Islands (Malvinas)
</option>
<option
value="FO"
>
Faroe Islands
</option>
<option
value="FJ"
>
Fiji
</option>
<option
value="FI"
>
Finland
</option>
<option
value="FR"
>
France
</option>
<option
value="GF"
>
French Guiana
</option>
<option
value="PF"
>
French Polynesia
</option>
<option
value="TF"
>
French Southern Territories
</option>
<option
value="GA"
>
Gabon
</option>
<option
value="GM"
>
Gambia
</option>
<option
value="GE"
>
Georgia
</option>
<option
value="DE"
>
Germany
</option>
<option
value="GH"
>
Ghana
</option>
<option
value="GI"
>
Gibraltar
</option>
<option
value="GR"
>
Greece
</option>
<option
value="GL"
>
Greenland
</option>
<option
value="GD"
>
Grenada
</option>
<option
value="GP"
>
Guadeloupe
</option>
<option
value="GU"
>
Guam
</option>
<option
value="GT"
>
Guatemala
</option>
<option
value="GN"
>
Guinea
</option>
<option
value="GW"
>
Guinea-Bissau
</option>
<option
value="GY"
>
Guyana
</option>
<option
value="HT"
>
Haiti
</option>
<option
value="HM"
>
Heard Island and Mcdonald Islands
</option>
<option
value="VA"
>
Holy See (Vatican City State)
</option>
<option
value="HN"
>
Honduras
</option>
<option
value="HK"
>
Hong Kong
</option>
<option
value="HU"
>
Hungary
</option>
<option
value="IS"
>
Iceland
</option>
<option
value="IN"
>
India
</option>
<option
value="ID"
>
Indonesia
</option>
<option
value="IR"
>
Iran, Islamic Republic of
</option>
<option
value="IQ"
>
Iraq
</option>
<option
value="IE"
>
Ireland
</option>
<option
value="IL"
>
Israel
</option>
<option
value="IT"
>
Italy
</option>
<option
value="JM"
>
Jamaica
</option>
<option
value="JP"
>
Japan
</option>
<option
value="JO"
>
Jordan
</option>
<option
value="KZ"
>
Kazakhstan
</option>
<option
value="KE"
>
Kenya
</option>
<option
value="KI"
>
Kiribati
</option>
<option
value="KP"
>
North Korea
</option>
<option
value="KR"
>
South Korea
</option>
<option
value="KW"
>
Kuwait
</option>
<option
value="KG"
>
Kyrgyzstan
</option>
<option
value="LA"
>
Lao People's Democratic Republic
</option>
<option
value="LV"
>
Latvia
</option>
<option
value="LB"
>
Lebanon
</option>
<option
value="LS"
>
Lesotho
</option>
<option
value="LR"
>
Liberia
</option>
<option
value="LY"
>
Libya
</option>
<option
value="LI"
>
Liechtenstein
</option>
<option
value="LT"
>
Lithuania
</option>
<option
value="LU"
>
Luxembourg
</option>
<option
value="MO"
>
Macao
</option>
<option
value="MG"
>
Madagascar
</option>
<option
value="MW"
>
Malawi
</option>
<option
value="MY"
>
Malaysia
</option>
<option
value="MV"
>
Maldives
</option>
<option
value="ML"
>
Mali
</option>
<option
value="MT"
>
Malta
</option>
<option
value="MH"
>
Marshall Islands
</option>
<option
value="MQ"
>
Martinique
</option>
<option
value="MR"
>
Mauritania
</option>
<option
value="MU"
>
Mauritius
</option>
<option
value="YT"
>
Mayotte
</option>
<option
value="MX"
>
Mexico
</option>
<option
value="FM"
>
Micronesia, Federated States of
</option>
<option
value="MD"
>
Moldova, Republic of
</option>
<option
value="MC"
>
Monaco
</option>
<option
value="MN"
>
Mongolia
</option>
<option
value="MS"
>
Montserrat
</option>
<option
value="MA"
>
Morocco
</option>
<option
value="MZ"
>
Mozambique
</option>
<option
value="MM"
>
Myanmar
</option>
<option
value="NA"
>
Namibia
</option>
<option
value="NR"
>
Nauru
</option>
<option
value="NP"
>
Nepal
</option>
<option
value="NL"
>
Netherlands
</option>
<option
value="NC"
>
New Caledonia
</option>
<option
value="NZ"
>
New Zealand
</option>
<option
value="NI"
>
Nicaragua
</option>
<option
value="NE"
>
Niger
</option>
<option
value="NG"
>
Nigeria
</option>
<option
value="NU"
>
Niue
</option>
<option
value="NF"
>
Norfolk Island
</option>
<option
value="MK"
>
North Macedonia, Republic of
</option>
<option
value="MP"
>
Northern Mariana Islands
</option>
<option
value="NO"
>
Norway
</option>
<option
value="OM"
>
Oman
</option>
<option
value="PK"
>
Pakistan
</option>
<option
value="PW"
>
Palau
</option>
<option
value="PS"
>
Palestinian Territory, Occupied
</option>
<option
value="PA"
>
Panama
</option>
<option
value="PG"
>
Papua New Guinea
</option>
<option
value="PY"
>
Paraguay
</option>
<option
value="PE"
>
Peru
</option>
<option
value="PH"
>
Philippines
</option>
<option
value="PN"
>
Pitcairn
</option>
<option
value="PL"
>
Poland
</option>
<option
value="PT"
>
Portugal
</option>
<option
value="PR"
>
Puerto Rico
</option>
<option
value="QA"
>
Qatar
</option>
<option
value="RE"
>
Reunion
</option>
<option
value="RO"
>
Romania
</option>
<option
value="RU"
>
Russian Federation
</option>
<option
value="RW"
>
Rwanda
</option>
<option
value="SH"
>
Saint Helena
</option>
<option
value="KN"
>
Saint Kitts and Nevis
</option>
<option
value="LC"
>
Saint Lucia
</option>
<option
value="PM"
>
Saint Pierre and Miquelon
</option>
<option
value="VC"
>
Saint Vincent and the Grenadines
</option>
<option
value="WS"
>
Samoa
</option>
<option
value="SM"
>
San Marino
</option>
<option
value="ST"
>
Sao Tome and Principe
</option>
<option
value="SA"
>
Saudi Arabia
</option>
<option
value="SN"
>
Senegal
</option>
<option
value="SC"
>
Seychelles
</option>
<option
value="SL"
>
Sierra Leone
</option>
<option
value="SG"
>
Singapore
</option>
<option
value="SK"
>
Slovakia
</option>
<option
value="SI"
>
Slovenia
</option>
<option
value="SB"
>
Solomon Islands
</option>
<option
value="SO"
>
Somalia
</option>
<option
value="ZA"
>
South Africa
</option>
<option
value="GS"
>
South Georgia and the South Sandwich Islands
</option>
<option
value="ES"
>
Spain
</option>
<option
value="LK"
>
Sri Lanka
</option>
<option
value="SD"
>
Sudan
</option>
<option
value="SR"
>
Suriname
</option>
<option
value="SJ"
>
Svalbard and Jan Mayen
</option>
<option
value="SZ"
>
Swaziland
</option>
<option
value="SE"
>
Sweden
</option>
<option
value="CH"
>
Switzerland
</option>
<option
value="SY"
>
Syrian Arab Republic
</option>
<option
value="TW"
>
Taiwan
</option>
<option
value="TJ"
>
Tajikistan
</option>
<option
value="TZ"
>
Tanzania, United Republic of
</option>
<option
value="TH"
>
Thailand
</option>
<option
value="TL"
>
Timor-Leste
</option>
<option
value="TG"
>
Togo
</option>
<option
value="TK"
>
Tokelau
</option>
<option
value="TO"
>
Tonga
</option>
<option
value="TT"
>
Trinidad and Tobago
</option>
<option
value="TN"
>
Tunisia
</option>
<option
value="TR"
>
Turkey
</option>
<option
value="TM"
>
Turkmenistan
</option>
<option
value="TC"
>
Turks and Caicos Islands
</option>
<option
value="TV"
>
Tuvalu
</option>
<option
value="UG"
>
Uganda
</option>
<option
value="UA"
>
Ukraine
</option>
<option
value="AE"
>
United Arab Emirates
</option>
<option
value="GB"
>
United Kingdom
</option>
<option
value="US"
>
United States of America
</option>
<option
value="UM"
>
United States Minor Outlying Islands
</option>
<option
value="UY"
>
Uruguay
</option>
<option
value="UZ"
>
Uzbekistan
</option>
<option
value="VU"
>
Vanuatu
</option>
<option
value="VE"
>
Venezuela
</option>
<option
value="VN"
>
Viet Nam
</option>
<option
value="VG"
>
Virgin Islands, British
</option>
<option
value="VI"
>
Virgin Islands, U.S.
</option>
<option
value="WF"
>
Wallis and Futuna
</option>
<option
value="EH"
>
Western Sahara
</option>
<option
value="YE"
>
Yemen
</option>
<option
value="ZM"
>
Zambia
</option>
<option
value="ZW"
>
Zimbabwe
</option>
<option
value="AX"
>
Åland Islands
</option>
<option
value="BQ"
>
Bonaire, Sint Eustatius and Saba
</option>
<option
value="CW"
>
Curaçao
</option>
<option
value="GG"
>
Guernsey
</option>
<option
value="IM"
>
Isle of Man
</option>
<option
value="JE"
>
Jersey
</option>
<option
value="ME"
>
Montenegro
</option>
<option
value="BL"
>
Saint Barthélemy
</option>
<option
value="MF"
>
Saint Martin (French part)
</option>
<option
value="RS"
>
Serbia
</option>
<option
value="SX"
>
Sint Maarten (Dutch part)
</option>
<option
value="SS"
>
South Sudan
</option>
<option
value="XK"
>
Kosovo
</option>
</select>
<div
class="pgn__form-control-description pgn__form-text pgn__form-text-invalid"
@@ -5976,1256 +4731,10 @@ exports[`<ProfilePage /> Renders correctly in various states test user with test
 
</option>
<option
value="AF"
>
Afghanistan
</option>
<option
value="AL"
>
Albania
</option>
<option
value="DZ"
>
Algeria
</option>
<option
value="AS"
>
American Samoa
</option>
<option
value="AD"
>
Andorra
</option>
<option
value="AO"
>
Angola
</option>
<option
value="AI"
>
Anguilla
</option>
<option
value="AQ"
>
Antarctica
</option>
<option
value="AG"
>
Antigua and Barbuda
</option>
<option
value="AR"
>
Argentina
</option>
<option
value="AM"
>
Armenia
</option>
<option
value="AW"
>
Aruba
</option>
<option
value="AU"
>
Australia
</option>
<option
value="AT"
>
Austria
</option>
<option
value="AZ"
>
Azerbaijan
</option>
<option
value="BS"
>
Bahamas
</option>
<option
value="BH"
>
Bahrain
</option>
<option
value="BD"
>
Bangladesh
</option>
<option
value="BB"
>
Barbados
</option>
<option
value="BY"
>
Belarus
</option>
<option
value="BE"
>
Belgium
</option>
<option
value="BZ"
>
Belize
</option>
<option
value="BJ"
>
Benin
</option>
<option
value="BM"
>
Bermuda
</option>
<option
value="BT"
>
Bhutan
</option>
<option
value="BO"
>
Bolivia
</option>
<option
value="BA"
>
Bosnia and Herzegovina
</option>
<option
value="BW"
>
Botswana
</option>
<option
value="BV"
>
Bouvet Island
</option>
<option
value="BR"
>
Brazil
</option>
<option
value="IO"
>
British Indian Ocean Territory
</option>
<option
value="BN"
>
Brunei Darussalam
</option>
<option
value="BG"
>
Bulgaria
</option>
<option
value="BF"
>
Burkina Faso
</option>
<option
value="BI"
>
Burundi
</option>
<option
value="KH"
>
Cambodia
</option>
<option
value="CM"
>
Cameroon
</option>
<option
value="CA"
>
Canada
</option>
<option
value="CV"
>
Cape Verde
</option>
<option
value="KY"
>
Cayman Islands
</option>
<option
value="CF"
>
Central African Republic
</option>
<option
value="TD"
>
Chad
</option>
<option
value="CL"
>
Chile
</option>
<option
value="CN"
>
China
</option>
<option
value="CX"
>
Christmas Island
</option>
<option
value="CC"
>
Cocos (Keeling) Islands
</option>
<option
value="CO"
>
Colombia
</option>
<option
value="KM"
>
Comoros
</option>
<option
value="CG"
>
Congo
</option>
<option
value="CD"
>
Congo, the Democratic Republic of the
</option>
<option
value="CK"
>
Cook Islands
</option>
<option
value="CR"
>
Costa Rica
</option>
<option
value="CI"
>
Cote D'Ivoire
</option>
<option
value="HR"
>
Croatia
</option>
<option
value="CU"
>
Cuba
</option>
<option
value="CY"
>
Cyprus
</option>
<option
value="CZ"
>
Czech Republic
</option>
<option
value="DK"
>
Denmark
</option>
<option
value="DJ"
>
Djibouti
</option>
<option
value="DM"
>
Dominica
</option>
<option
value="DO"
>
Dominican Republic
</option>
<option
value="EC"
>
Ecuador
</option>
<option
value="EG"
>
Egypt
</option>
<option
value="SV"
>
El Salvador
</option>
<option
value="GQ"
>
Equatorial Guinea
</option>
<option
value="ER"
>
Eritrea
</option>
<option
value="EE"
>
Estonia
</option>
<option
value="ET"
>
Ethiopia
</option>
<option
value="FK"
>
Falkland Islands (Malvinas)
</option>
<option
value="FO"
>
Faroe Islands
</option>
<option
value="FJ"
>
Fiji
</option>
<option
value="FI"
>
Finland
</option>
<option
value="FR"
>
France
</option>
<option
value="GF"
>
French Guiana
</option>
<option
value="PF"
>
French Polynesia
</option>
<option
value="TF"
>
French Southern Territories
</option>
<option
value="GA"
>
Gabon
</option>
<option
value="GM"
>
Gambia
</option>
<option
value="GE"
>
Georgia
</option>
<option
value="DE"
>
Germany
</option>
<option
value="GH"
>
Ghana
</option>
<option
value="GI"
>
Gibraltar
</option>
<option
value="GR"
>
Greece
</option>
<option
value="GL"
>
Greenland
</option>
<option
value="GD"
>
Grenada
</option>
<option
value="GP"
>
Guadeloupe
</option>
<option
value="GU"
>
Guam
</option>
<option
value="GT"
>
Guatemala
</option>
<option
value="GN"
>
Guinea
</option>
<option
value="GW"
>
Guinea-Bissau
</option>
<option
value="GY"
>
Guyana
</option>
<option
value="HT"
>
Haiti
</option>
<option
value="HM"
>
Heard Island and Mcdonald Islands
</option>
<option
value="VA"
>
Holy See (Vatican City State)
</option>
<option
value="HN"
>
Honduras
</option>
<option
value="HK"
>
Hong Kong
</option>
<option
value="HU"
>
Hungary
</option>
<option
value="IS"
>
Iceland
</option>
<option
value="IN"
>
India
</option>
<option
value="ID"
>
Indonesia
</option>
<option
value="IR"
>
Iran, Islamic Republic of
</option>
<option
value="IQ"
>
Iraq
</option>
<option
value="IE"
>
Ireland
</option>
<option
value="IL"
>
Israel
</option>
<option
value="IT"
>
Italy
</option>
<option
value="JM"
>
Jamaica
</option>
<option
value="JP"
>
Japan
</option>
<option
value="JO"
>
Jordan
</option>
<option
value="KZ"
>
Kazakhstan
</option>
<option
value="KE"
>
Kenya
</option>
<option
value="KI"
>
Kiribati
</option>
<option
value="KP"
>
North Korea
</option>
<option
value="KR"
>
South Korea
</option>
<option
value="KW"
>
Kuwait
</option>
<option
value="KG"
>
Kyrgyzstan
</option>
<option
value="LA"
>
Lao People's Democratic Republic
</option>
<option
value="LV"
>
Latvia
</option>
<option
value="LB"
>
Lebanon
</option>
<option
value="LS"
>
Lesotho
</option>
<option
value="LR"
>
Liberia
</option>
<option
value="LY"
>
Libya
</option>
<option
value="LI"
>
Liechtenstein
</option>
<option
value="LT"
>
Lithuania
</option>
<option
value="LU"
>
Luxembourg
</option>
<option
value="MO"
>
Macao
</option>
<option
value="MG"
>
Madagascar
</option>
<option
value="MW"
>
Malawi
</option>
<option
value="MY"
>
Malaysia
</option>
<option
value="MV"
>
Maldives
</option>
<option
value="ML"
>
Mali
</option>
<option
value="MT"
>
Malta
</option>
<option
value="MH"
>
Marshall Islands
</option>
<option
value="MQ"
>
Martinique
</option>
<option
value="MR"
>
Mauritania
</option>
<option
value="MU"
>
Mauritius
</option>
<option
value="YT"
>
Mayotte
</option>
<option
value="MX"
>
Mexico
</option>
<option
value="FM"
>
Micronesia, Federated States of
</option>
<option
value="MD"
>
Moldova, Republic of
</option>
<option
value="MC"
>
Monaco
</option>
<option
value="MN"
>
Mongolia
</option>
<option
value="MS"
>
Montserrat
</option>
<option
value="MA"
>
Morocco
</option>
<option
value="MZ"
>
Mozambique
</option>
<option
value="MM"
>
Myanmar
</option>
<option
value="NA"
>
Namibia
</option>
<option
value="NR"
>
Nauru
</option>
<option
value="NP"
>
Nepal
</option>
<option
value="NL"
>
Netherlands
</option>
<option
value="NC"
>
New Caledonia
</option>
<option
value="NZ"
>
New Zealand
</option>
<option
value="NI"
>
Nicaragua
</option>
<option
value="NE"
>
Niger
</option>
<option
value="NG"
>
Nigeria
</option>
<option
value="NU"
>
Niue
</option>
<option
value="NF"
>
Norfolk Island
</option>
<option
value="MK"
>
North Macedonia, Republic of
</option>
<option
value="MP"
>
Northern Mariana Islands
</option>
<option
value="NO"
>
Norway
</option>
<option
value="OM"
>
Oman
</option>
<option
value="PK"
>
Pakistan
</option>
<option
value="PW"
>
Palau
</option>
<option
value="PS"
>
Palestinian Territory, Occupied
</option>
<option
value="PA"
>
Panama
</option>
<option
value="PG"
>
Papua New Guinea
</option>
<option
value="PY"
>
Paraguay
</option>
<option
value="PE"
>
Peru
</option>
<option
value="PH"
>
Philippines
</option>
<option
value="PN"
>
Pitcairn
</option>
<option
value="PL"
>
Poland
</option>
<option
value="PT"
>
Portugal
</option>
<option
value="PR"
>
Puerto Rico
</option>
<option
value="QA"
>
Qatar
</option>
<option
value="RE"
>
Reunion
</option>
<option
value="RO"
>
Romania
</option>
<option
disabled=""
value="RU"
>
Russian Federation
</option>
<option
value="RW"
>
Rwanda
</option>
<option
value="SH"
>
Saint Helena
</option>
<option
value="KN"
>
Saint Kitts and Nevis
</option>
<option
value="LC"
>
Saint Lucia
</option>
<option
value="PM"
>
Saint Pierre and Miquelon
</option>
<option
value="VC"
>
Saint Vincent and the Grenadines
</option>
<option
value="WS"
>
Samoa
</option>
<option
value="SM"
>
San Marino
</option>
<option
value="ST"
>
Sao Tome and Principe
</option>
<option
value="SA"
>
Saudi Arabia
</option>
<option
value="SN"
>
Senegal
</option>
<option
value="SC"
>
Seychelles
</option>
<option
value="SL"
>
Sierra Leone
</option>
<option
value="SG"
>
Singapore
</option>
<option
value="SK"
>
Slovakia
</option>
<option
value="SI"
>
Slovenia
</option>
<option
value="SB"
>
Solomon Islands
</option>
<option
value="SO"
>
Somalia
</option>
<option
value="ZA"
>
South Africa
</option>
<option
value="GS"
>
South Georgia and the South Sandwich Islands
</option>
<option
value="ES"
>
Spain
</option>
<option
value="LK"
>
Sri Lanka
</option>
<option
value="SD"
>
Sudan
</option>
<option
value="SR"
>
Suriname
</option>
<option
value="SJ"
>
Svalbard and Jan Mayen
</option>
<option
value="SZ"
>
Swaziland
</option>
<option
value="SE"
>
Sweden
</option>
<option
value="CH"
>
Switzerland
</option>
<option
value="SY"
>
Syrian Arab Republic
</option>
<option
value="TW"
>
Taiwan
</option>
<option
value="TJ"
>
Tajikistan
</option>
<option
value="TZ"
>
Tanzania, United Republic of
</option>
<option
value="TH"
>
Thailand
</option>
<option
value="TL"
>
Timor-Leste
</option>
<option
value="TG"
>
Togo
</option>
<option
value="TK"
>
Tokelau
</option>
<option
value="TO"
>
Tonga
</option>
<option
value="TT"
>
Trinidad and Tobago
</option>
<option
value="TN"
>
Tunisia
</option>
<option
value="TR"
>
Turkey
</option>
<option
value="TM"
>
Turkmenistan
</option>
<option
value="TC"
>
Turks and Caicos Islands
</option>
<option
value="TV"
>
Tuvalu
</option>
<option
value="UG"
>
Uganda
</option>
<option
value="UA"
>
Ukraine
</option>
<option
value="AE"
>
United Arab Emirates
</option>
<option
value="GB"
>
United Kingdom
</option>
<option
value="US"
>
United States of America
</option>
<option
value="UM"
>
United States Minor Outlying Islands
</option>
<option
value="UY"
>
Uruguay
</option>
<option
value="UZ"
>
Uzbekistan
</option>
<option
value="VU"
>
Vanuatu
</option>
<option
value="VE"
>
Venezuela
</option>
<option
value="VN"
>
Viet Nam
</option>
<option
value="VG"
>
Virgin Islands, British
</option>
<option
value="VI"
>
Virgin Islands, U.S.
</option>
<option
value="WF"
>
Wallis and Futuna
</option>
<option
value="EH"
>
Western Sahara
</option>
<option
value="YE"
>
Yemen
</option>
<option
value="ZM"
>
Zambia
</option>
<option
value="ZW"
>
Zimbabwe
</option>
<option
value="AX"
>
Åland Islands
</option>
<option
value="BQ"
>
Bonaire, Sint Eustatius and Saba
</option>
<option
value="CW"
>
Curaçao
</option>
<option
value="GG"
>
Guernsey
</option>
<option
value="IM"
>
Isle of Man
</option>
<option
value="JE"
>
Jersey
</option>
<option
value="ME"
>
Montenegro
</option>
<option
value="BL"
>
Saint Barthélemy
</option>
<option
value="MF"
>
Saint Martin (French part)
</option>
<option
value="RS"
>
Serbia
</option>
<option
value="SX"
>
Sint Maarten (Dutch part)
</option>
<option
value="SS"
>
South Sudan
</option>
<option
value="XK"
>
Kosovo
</option>
</select>
</div>
<div
@@ -8093,1251 +5602,11 @@ exports[`<ProfilePage /> Renders correctly in various states test user with test
>
 
</option>
<option
value="AF"
>
Afghanistan
</option>
<option
value="AL"
>
Albania
</option>
<option
value="DZ"
>
Algeria
</option>
<option
value="AS"
>
American Samoa
</option>
<option
value="AD"
>
Andorra
</option>
<option
value="AO"
>
Angola
</option>
<option
value="AI"
>
Anguilla
</option>
<option
value="AQ"
>
Antarctica
</option>
<option
value="AG"
>
Antigua and Barbuda
</option>
<option
value="AR"
>
Argentina
</option>
<option
value="AM"
>
Armenia
</option>
<option
value="AW"
>
Aruba
</option>
<option
value="AU"
>
Australia
</option>
<option
value="AT"
>
Austria
</option>
<option
value="AZ"
>
Azerbaijan
</option>
<option
value="BS"
>
Bahamas
</option>
<option
value="BH"
>
Bahrain
</option>
<option
value="BD"
>
Bangladesh
</option>
<option
value="BB"
>
Barbados
</option>
<option
value="BY"
>
Belarus
</option>
<option
value="BE"
>
Belgium
</option>
<option
value="BZ"
>
Belize
</option>
<option
value="BJ"
>
Benin
</option>
<option
value="BM"
>
Bermuda
</option>
<option
value="BT"
>
Bhutan
</option>
<option
value="BO"
>
Bolivia
</option>
<option
value="BA"
>
Bosnia and Herzegovina
</option>
<option
value="BW"
>
Botswana
</option>
<option
value="BV"
>
Bouvet Island
</option>
<option
value="BR"
>
Brazil
</option>
<option
value="IO"
>
British Indian Ocean Territory
</option>
<option
value="BN"
>
Brunei Darussalam
</option>
<option
value="BG"
>
Bulgaria
</option>
<option
value="BF"
>
Burkina Faso
</option>
<option
value="BI"
>
Burundi
</option>
<option
value="KH"
>
Cambodia
</option>
<option
value="CM"
>
Cameroon
</option>
<option
value="CA"
>
Canada
</option>
<option
value="CV"
>
Cape Verde
</option>
<option
value="KY"
>
Cayman Islands
</option>
<option
value="CF"
>
Central African Republic
</option>
<option
value="TD"
>
Chad
</option>
<option
value="CL"
>
Chile
</option>
<option
value="CN"
>
China
</option>
<option
value="CX"
>
Christmas Island
</option>
<option
value="CC"
>
Cocos (Keeling) Islands
</option>
<option
value="CO"
>
Colombia
</option>
<option
value="KM"
>
Comoros
</option>
<option
value="CG"
>
Congo
</option>
<option
value="CD"
>
Congo, the Democratic Republic of the
</option>
<option
value="CK"
>
Cook Islands
</option>
<option
value="CR"
>
Costa Rica
</option>
<option
value="CI"
>
Cote D'Ivoire
</option>
<option
value="HR"
>
Croatia
</option>
<option
value="CU"
>
Cuba
</option>
<option
value="CY"
>
Cyprus
</option>
<option
value="CZ"
>
Czech Republic
</option>
<option
value="DK"
>
Denmark
</option>
<option
value="DJ"
>
Djibouti
</option>
<option
value="DM"
>
Dominica
</option>
<option
value="DO"
>
Dominican Republic
</option>
<option
value="EC"
>
Ecuador
</option>
<option
value="EG"
>
Egypt
</option>
<option
value="SV"
>
El Salvador
</option>
<option
value="GQ"
>
Equatorial Guinea
</option>
<option
value="ER"
>
Eritrea
</option>
<option
value="EE"
>
Estonia
</option>
<option
value="ET"
>
Ethiopia
</option>
<option
value="FK"
>
Falkland Islands (Malvinas)
</option>
<option
value="FO"
>
Faroe Islands
</option>
<option
value="FJ"
>
Fiji
</option>
<option
value="FI"
>
Finland
</option>
<option
value="FR"
>
France
</option>
<option
value="GF"
>
French Guiana
</option>
<option
value="PF"
>
French Polynesia
</option>
<option
value="TF"
>
French Southern Territories
</option>
<option
value="GA"
>
Gabon
</option>
<option
value="GM"
>
Gambia
</option>
<option
value="GE"
>
Georgia
</option>
<option
value="DE"
>
Germany
</option>
<option
value="GH"
>
Ghana
</option>
<option
value="GI"
>
Gibraltar
</option>
<option
value="GR"
>
Greece
</option>
<option
value="GL"
>
Greenland
</option>
<option
value="GD"
>
Grenada
</option>
<option
value="GP"
>
Guadeloupe
</option>
<option
value="GU"
>
Guam
</option>
<option
value="GT"
>
Guatemala
</option>
<option
value="GN"
>
Guinea
</option>
<option
value="GW"
>
Guinea-Bissau
</option>
<option
value="GY"
>
Guyana
</option>
<option
value="HT"
>
Haiti
</option>
<option
value="HM"
>
Heard Island and Mcdonald Islands
</option>
<option
value="VA"
>
Holy See (Vatican City State)
</option>
<option
value="HN"
>
Honduras
</option>
<option
value="HK"
>
Hong Kong
</option>
<option
value="HU"
>
Hungary
</option>
<option
value="IS"
>
Iceland
</option>
<option
value="IN"
>
India
</option>
<option
value="ID"
>
Indonesia
</option>
<option
value="IR"
>
Iran, Islamic Republic of
</option>
<option
value="IQ"
>
Iraq
</option>
<option
value="IE"
>
Ireland
</option>
<option
value="IL"
>
Israel
</option>
<option
value="IT"
>
Italy
</option>
<option
value="JM"
>
Jamaica
</option>
<option
value="JP"
>
Japan
</option>
<option
value="JO"
>
Jordan
</option>
<option
value="KZ"
>
Kazakhstan
</option>
<option
value="KE"
>
Kenya
</option>
<option
value="KI"
>
Kiribati
</option>
<option
value="KP"
>
North Korea
</option>
<option
value="KR"
>
South Korea
</option>
<option
value="KW"
>
Kuwait
</option>
<option
value="KG"
>
Kyrgyzstan
</option>
<option
value="LA"
>
Lao People's Democratic Republic
</option>
<option
value="LV"
>
Latvia
</option>
<option
value="LB"
>
Lebanon
</option>
<option
value="LS"
>
Lesotho
</option>
<option
value="LR"
>
Liberia
</option>
<option
value="LY"
>
Libya
</option>
<option
value="LI"
>
Liechtenstein
</option>
<option
value="LT"
>
Lithuania
</option>
<option
value="LU"
>
Luxembourg
</option>
<option
value="MO"
>
Macao
</option>
<option
value="MG"
>
Madagascar
</option>
<option
value="MW"
>
Malawi
</option>
<option
value="MY"
>
Malaysia
</option>
<option
value="MV"
>
Maldives
</option>
<option
value="ML"
>
Mali
</option>
<option
value="MT"
>
Malta
</option>
<option
value="MH"
>
Marshall Islands
</option>
<option
value="MQ"
>
Martinique
</option>
<option
value="MR"
>
Mauritania
</option>
<option
value="MU"
>
Mauritius
</option>
<option
value="YT"
>
Mayotte
</option>
<option
value="MX"
>
Mexico
</option>
<option
value="FM"
>
Micronesia, Federated States of
</option>
<option
value="MD"
>
Moldova, Republic of
</option>
<option
value="MC"
>
Monaco
</option>
<option
value="MN"
>
Mongolia
</option>
<option
value="MS"
>
Montserrat
</option>
<option
value="MA"
>
Morocco
</option>
<option
value="MZ"
>
Mozambique
</option>
<option
value="MM"
>
Myanmar
</option>
<option
value="NA"
>
Namibia
</option>
<option
value="NR"
>
Nauru
</option>
<option
value="NP"
>
Nepal
</option>
<option
value="NL"
>
Netherlands
</option>
<option
value="NC"
>
New Caledonia
</option>
<option
value="NZ"
>
New Zealand
</option>
<option
value="NI"
>
Nicaragua
</option>
<option
value="NE"
>
Niger
</option>
<option
value="NG"
>
Nigeria
</option>
<option
value="NU"
>
Niue
</option>
<option
value="NF"
>
Norfolk Island
</option>
<option
value="MK"
>
North Macedonia, Republic of
</option>
<option
value="MP"
>
Northern Mariana Islands
</option>
<option
value="NO"
>
Norway
</option>
<option
value="OM"
>
Oman
</option>
<option
value="PK"
>
Pakistan
</option>
<option
value="PW"
>
Palau
</option>
<option
value="PS"
>
Palestinian Territory, Occupied
</option>
<option
value="PA"
>
Panama
</option>
<option
value="PG"
>
Papua New Guinea
</option>
<option
value="PY"
>
Paraguay
</option>
<option
value="PE"
>
Peru
</option>
<option
value="PH"
>
Philippines
</option>
<option
value="PN"
>
Pitcairn
</option>
<option
value="PL"
>
Poland
</option>
<option
value="PT"
>
Portugal
</option>
<option
value="PR"
>
Puerto Rico
</option>
<option
value="QA"
>
Qatar
</option>
<option
value="RE"
>
Reunion
</option>
<option
value="RO"
>
Romania
</option>
<option
value="RW"
>
Rwanda
</option>
<option
value="SH"
>
Saint Helena
</option>
<option
value="KN"
>
Saint Kitts and Nevis
</option>
<option
value="LC"
>
Saint Lucia
</option>
<option
value="PM"
>
Saint Pierre and Miquelon
</option>
<option
value="VC"
>
Saint Vincent and the Grenadines
</option>
<option
value="WS"
>
Samoa
</option>
<option
value="SM"
>
San Marino
</option>
<option
value="ST"
>
Sao Tome and Principe
</option>
<option
value="SA"
>
Saudi Arabia
</option>
<option
value="SN"
>
Senegal
</option>
<option
value="SC"
>
Seychelles
</option>
<option
value="SL"
>
Sierra Leone
</option>
<option
value="SG"
>
Singapore
</option>
<option
value="SK"
>
Slovakia
</option>
<option
value="SI"
>
Slovenia
</option>
<option
value="SB"
>
Solomon Islands
</option>
<option
value="SO"
>
Somalia
</option>
<option
value="ZA"
>
South Africa
</option>
<option
value="GS"
>
South Georgia and the South Sandwich Islands
</option>
<option
value="ES"
>
Spain
</option>
<option
value="LK"
>
Sri Lanka
</option>
<option
value="SD"
>
Sudan
</option>
<option
value="SR"
>
Suriname
</option>
<option
value="SJ"
>
Svalbard and Jan Mayen
</option>
<option
value="SZ"
>
Swaziland
</option>
<option
value="SE"
>
Sweden
</option>
<option
value="CH"
>
Switzerland
</option>
<option
value="SY"
>
Syrian Arab Republic
</option>
<option
value="TW"
>
Taiwan
</option>
<option
value="TJ"
>
Tajikistan
</option>
<option
value="TZ"
>
Tanzania, United Republic of
</option>
<option
value="TH"
>
Thailand
</option>
<option
value="TL"
>
Timor-Leste
</option>
<option
value="TG"
>
Togo
</option>
<option
value="TK"
>
Tokelau
</option>
<option
value="TO"
>
Tonga
</option>
<option
value="TT"
>
Trinidad and Tobago
</option>
<option
value="TN"
>
Tunisia
</option>
<option
value="TR"
>
Turkey
</option>
<option
value="TM"
>
Turkmenistan
</option>
<option
value="TC"
>
Turks and Caicos Islands
</option>
<option
value="TV"
>
Tuvalu
</option>
<option
value="UG"
>
Uganda
</option>
<option
value="UA"
>
Ukraine
</option>
<option
value="AE"
>
United Arab Emirates
</option>
<option
value="GB"
>
United Kingdom
</option>
<option
value="US"
>
United States of America
</option>
<option
value="UM"
>
United States Minor Outlying Islands
</option>
<option
value="UY"
>
Uruguay
</option>
<option
value="UZ"
>
Uzbekistan
</option>
<option
value="VU"
>
Vanuatu
</option>
<option
value="VE"
>
Venezuela
</option>
<option
value="VN"
>
Viet Nam
</option>
<option
value="VG"
>
Virgin Islands, British
</option>
<option
value="VI"
>
Virgin Islands, U.S.
</option>
<option
value="WF"
>
Wallis and Futuna
</option>
<option
value="EH"
>
Western Sahara
</option>
<option
value="YE"
>
Yemen
</option>
<option
value="ZM"
>
Zambia
</option>
<option
value="ZW"
>
Zimbabwe
</option>
<option
value="AX"
>
Åland Islands
</option>
<option
value="BQ"
>
Bonaire, Sint Eustatius and Saba
</option>
<option
value="CW"
>
Curaçao
</option>
<option
value="GG"
>
Guernsey
</option>
<option
value="IM"
>
Isle of Man
</option>
<option
value="JE"
>
Jersey
</option>
<option
value="ME"
>
Montenegro
</option>
<option
value="BL"
>
Saint Barthélemy
</option>
<option
value="MF"
>
Saint Martin (French part)
</option>
<option
value="RS"
>
Serbia
</option>
<option
value="SX"
>
Sint Maarten (Dutch part)
</option>
<option
value="SS"
>
South Sudan
</option>
<option
value="XK"
>
Kosovo
</option>
</select>
</div>
<div

View File

@@ -25,12 +25,14 @@ export const fetchProfileSuccess = (
preferences,
courseCertificates,
isAuthenticatedUserProfile,
countriesCodesList,
) => ({
type: FETCH_PROFILE.SUCCESS,
account,
preferences,
courseCertificates,
isAuthenticatedUserProfile,
countriesCodesList,
});
export const fetchProfileReset = () => ({

View File

@@ -22,7 +22,12 @@ const SOCIAL = {
},
};
const FIELD_LABELS = {
COUNTRY: 'country',
};
export {
EDUCATION_LEVELS,
SOCIAL,
FIELD_LABELS,
};

View File

@@ -22,7 +22,7 @@ export const initialState = {
drafts: {},
isLoadingProfile: true,
isAuthenticatedUserProfile: false,
disabledCountries: ['RU'],
countriesCodesList: [],
};
const profilePage = (state = initialState, action = {}) => {
@@ -43,6 +43,7 @@ const profilePage = (state = initialState, action = {}) => {
courseCertificates: action.courseCertificates,
isLoadingProfile: false,
isAuthenticatedUserProfile: action.isAuthenticatedUserProfile,
countriesCodesList: action.countriesCodesList,
};
case SAVE_PROFILE.BEGIN:
return {

View File

@@ -41,6 +41,7 @@ export function* handleFetchProfile(action) {
let preferences = {};
let account = userAccount;
let courseCertificates = null;
let countriesCodesList = [];
try {
yield put(fetchProfileBegin());
@@ -49,6 +50,7 @@ export function* handleFetchProfile(action) {
const calls = [
call(ProfileApiService.getAccount, username),
call(ProfileApiService.getCourseCertificates, username),
call(ProfileApiService.getCountryList),
];
if (isAuthenticatedUserProfile) {
@@ -61,9 +63,9 @@ export function* handleFetchProfile(action) {
const result = yield all(calls);
if (isAuthenticatedUserProfile) {
[account, courseCertificates, preferences] = result;
[account, courseCertificates, countriesCodesList, preferences] = result;
} else {
[account, courseCertificates] = result;
[account, courseCertificates, countriesCodesList] = result;
}
// Set initial visibility values for account
@@ -89,6 +91,7 @@ export function* handleFetchProfile(action) {
preferences,
courseCertificates,
isAuthenticatedUserProfile,
countriesCodesList,
));
yield put(fetchProfileReset());

View File

@@ -19,6 +19,7 @@ jest.mock('./services', () => ({
getPreferences: jest.fn(),
getAccount: jest.fn(),
getCourseCertificates: jest.fn(),
getCountryList: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
@@ -68,17 +69,19 @@ describe('RootSaga', () => {
const action = profileActions.fetchProfile('gonzo');
const gen = handleFetchProfile(action);
const result = [userAccount, [1, 2, 3], { preferences: 'stuff' }];
const result = [userAccount, [1, 2, 3], [], { preferences: 'stuff' }];
expect(gen.next().value).toEqual(select(userAccountSelector));
expect(gen.next(selectorData).value).toEqual(put(profileActions.fetchProfileBegin()));
expect(gen.next().value).toEqual(all([
call(ProfileApiService.getAccount, 'gonzo'),
call(ProfileApiService.getCourseCertificates, 'gonzo'),
call(ProfileApiService.getCountryList),
call(ProfileApiService.getPreferences, 'gonzo'),
]));
expect(gen.next(result).value)
.toEqual(put(profileActions.fetchProfileSuccess(userAccount, result[2], result[1], true)));
.toEqual(put(profileActions.fetchProfileSuccess(userAccount, result[3], result[1], true, [])));
expect(gen.next().value).toEqual(put(profileActions.fetchProfileReset()));
expect(gen.next().value).toBeUndefined();
});
@@ -88,6 +91,7 @@ describe('RootSaga', () => {
username: 'gonzo',
other: 'data',
};
const countriesCodesList = [{ code: 'AX' }, { code: 'AL' }];
getAuthenticatedUser.mockReturnValue(userAccount);
const selectorData = {
userAccount,
@@ -96,16 +100,17 @@ describe('RootSaga', () => {
const action = profileActions.fetchProfile('booyah');
const gen = handleFetchProfile(action);
const result = [{}, [1, 2, 3]];
const result = [{}, [1, 2, 3], countriesCodesList];
expect(gen.next().value).toEqual(select(userAccountSelector));
expect(gen.next(selectorData).value).toEqual(put(profileActions.fetchProfileBegin()));
expect(gen.next().value).toEqual(all([
call(ProfileApiService.getAccount, 'booyah'),
call(ProfileApiService.getCourseCertificates, 'booyah'),
call(ProfileApiService.getCountryList),
]));
expect(gen.next(result).value)
.toEqual(put(profileActions.fetchProfileSuccess(result[0], {}, result[1], false)));
.toEqual(put(profileActions.fetchProfileSuccess(result[0], {}, result[1], false, countriesCodesList)));
expect(gen.next().value).toEqual(put(profileActions.fetchProfileReset()));
expect(gen.next().value).toBeUndefined();
});

View File

@@ -23,7 +23,7 @@ export const isLoadingProfileSelector = state => state.profilePage.isLoadingProf
export const currentlyEditingFieldSelector = state => state.profilePage.currentlyEditingField;
export const accountErrorsSelector = state => state.profilePage.errors;
export const isAuthenticatedUserProfileSelector = state => state.profilePage.isAuthenticatedUserProfile;
export const disabledCountriesSelector = state => state.profilePage.disabledCountries;
export const countriesCodesListSelector = state => state.profilePage.countriesCodesList;
export const editableFormModeSelector = createSelector(
profileAccountSelector,
@@ -113,7 +113,14 @@ export const sortedLanguagesSelector = createSelector(
export const sortedCountriesSelector = createSelector(
localeSelector,
locale => getCountryList(locale),
countriesCodesListSelector,
profileAccountSelector,
(locale, countriesCodesList, profileAccount) => {
const countryList = getCountryList(locale);
const userCountry = profileAccount.country;
return countryList.filter(({ code }) => code === userCountry || countriesCodesList.find(x => x === code));
},
);
export const preferredLanguageSelector = createSelector(
@@ -131,13 +138,13 @@ export const countrySelector = createSelector(
editableFormSelector,
sortedCountriesSelector,
countryMessagesSelector,
disabledCountriesSelector,
countriesCodesListSelector,
profileAccountSelector,
(editableForm, sortedCountries, countryMessages, disabledCountries, account) => ({
(editableForm, translatedCountries, countryMessages, countriesCodesList, account) => ({
...editableForm,
sortedCountries,
translatedCountries,
countryMessages,
disabledCountries,
countriesCodesList,
committedCountry: account.country,
}),
);

View File

@@ -2,6 +2,7 @@ import { ensureConfig, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient as getHttpClient } from '@edx/frontend-platform/auth';
import { logError } from '@edx/frontend-platform/logging';
import { camelCaseObject, convertKeyNames, snakeCaseObject } from '../utils';
import { FIELD_LABELS } from './constants';
ensureConfig(['LMS_BASE_URL'], 'Profile API service');
@@ -147,3 +148,21 @@ export async function getCourseCertificates(username) {
return [];
}
}
function extractCountryList(data) {
return data?.fields
.find(({ name }) => name === FIELD_LABELS.COUNTRY)
?.options?.map(({ value }) => (value)) || [];
}
export async function getCountryList() {
const url = `${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`;
try {
const { data } = await getHttpClient().get(url);
return extractCountryList(data);
} catch (e) {
logError(e);
return [];
}
}

View File

@@ -23,6 +23,7 @@ class Country extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleOpen = this.handleOpen.bind(this);
this.isDisabledCountry = this.isDisabledCountry.bind(this);
}
handleChange(e) {
@@ -35,13 +36,7 @@ class Country extends React.Component {
handleSubmit(e) {
e.preventDefault();
const {
country, disabledCountries, formId, submitHandler,
} = this.props;
if (!disabledCountries.includes(country)) {
submitHandler(formId);
}
this.props.submitHandler(this.props.formId);
}
handleClose() {
@@ -53,23 +48,9 @@ class Country extends React.Component {
}
isDisabledCountry = (country) => {
const { disabledCountries } = this.props;
return disabledCountries.includes(country);
};
const { countriesCodesList } = this.props;
filteredCountries = (countryList) => {
const { disabledCountries, committedCountry } = this.props;
if (!disabledCountries.length) {
return countryList;
}
return countryList.filter(({ code }) => {
const isDisabled = this.isDisabledCountry(code);
const isUserCountry = code === committedCountry;
return !isDisabled || isUserCountry;
});
return countriesCodesList.length > 0 && !countriesCodesList.find(code => code === country);
};
render() {
@@ -81,10 +62,9 @@ class Country extends React.Component {
saveState,
error,
intl,
sortedCountries,
translatedCountries,
countryMessages,
} = this.props;
const filteredCountries = this.filteredCountries(sortedCountries);
return (
<SwitchContent
@@ -111,7 +91,7 @@ class Country extends React.Component {
onChange={this.handleChange}
>
<option value="">&nbsp;</option>
{filteredCountries.map(({ code, name }) => (
{translatedCountries.map(({ code, name }) => (
<option key={code} value={code} disabled={this.isDisabledCountry(code)}>{name}</option>
))}
</select>
@@ -180,13 +160,12 @@ Country.propTypes = {
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
sortedCountries: PropTypes.arrayOf(PropTypes.shape({
translatedCountries: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})).isRequired,
disabledCountries: PropTypes.arrayOf(PropTypes.string),
countriesCodesList: PropTypes.arrayOf(PropTypes.string).isRequired,
countryMessages: PropTypes.objectOf(PropTypes.string).isRequired,
committedCountry: PropTypes.string,
// Actions
changeHandler: PropTypes.func.isRequired,
@@ -204,8 +183,6 @@ Country.defaultProps = {
country: null,
visibilityCountry: 'private',
error: null,
disabledCountries: [],
committedCountry: '',
};
export default connect(

View File

@@ -1,9 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
AuthenticatedPageRoute,
PageWrap,
} from '@edx/frontend-platform/react';
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';