feat: removed flag for new profile UI and depreacted old code (#1233)
This commit is contained in:
1
.env
1
.env
@@ -31,5 +31,4 @@ SEARCH_CATALOG_URL=''
|
||||
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
ENABLE_NEW_PROFILE_VIEW=''
|
||||
DISABLE_VISIBILITY_EDITING=''
|
||||
|
||||
@@ -32,5 +32,4 @@ SEARCH_CATALOG_URL='http://localhost:18000/courses'
|
||||
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
ENABLE_NEW_PROFILE_VIEW=''
|
||||
DISABLE_VISIBILITY_EDITING=''
|
||||
|
||||
@@ -26,5 +26,4 @@ COLLECT_YEAR_OF_BIRTH=true
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
PARAGON_THEME_URLS={}
|
||||
ENABLE_NEW_PROFILE_VIEW=''
|
||||
DISABLE_VISIBILITY_EDITING=''
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { reducer as profilePageReducer } from '../profile';
|
||||
import { reducer as newProfilePageReducer } from '../profile-v2';
|
||||
|
||||
const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW;
|
||||
|
||||
const createRootReducer = () => combineReducers({
|
||||
profilePage: isNewProfileEnabled ? newProfilePageReducer : profilePageReducer,
|
||||
profilePage: profilePageReducer,
|
||||
});
|
||||
|
||||
export default createRootReducer;
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { all } from 'redux-saga/effects';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { saga as profileSaga } from '../profile';
|
||||
import { saga as newProfileSaga } from '../profile-v2';
|
||||
|
||||
const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW;
|
||||
|
||||
export default function* rootSaga() {
|
||||
yield all([
|
||||
isNewProfileEnabled ? newProfileSaga() : profileSaga(),
|
||||
profileSaga(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
|
||||
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
@import "~@edx/frontend-component-footer/dist/footer";
|
||||
|
||||
@import './profile-v2/index';
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
initialize,
|
||||
mergeConfig,
|
||||
subscribe,
|
||||
getConfig,
|
||||
} from '@edx/frontend-platform';
|
||||
import {
|
||||
AppProvider,
|
||||
@@ -28,20 +27,16 @@ import Head from './head/Head';
|
||||
|
||||
import AppRoutes from './routes/AppRoutes';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const rootNode = createRoot(document.getElementById('root'));
|
||||
subscribe(APP_READY, async () => {
|
||||
const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW === 'true';
|
||||
if (isNewProfileEnabled) {
|
||||
await import('./index-v2.scss');
|
||||
} else {
|
||||
await import('./index.scss');
|
||||
}
|
||||
rootNode.render(
|
||||
<AppProvider store={configureStore()}>
|
||||
<Head />
|
||||
<Header />
|
||||
<main id="main">
|
||||
<AppRoutes isNewProfileEnabled={isNewProfileEnabled} />
|
||||
<AppRoutes />
|
||||
</main>
|
||||
<FooterSlot />
|
||||
</AppProvider>,
|
||||
@@ -61,7 +56,6 @@ initialize({
|
||||
mergeConfig({
|
||||
COLLECT_YEAR_OF_BIRTH: process.env.COLLECT_YEAR_OF_BIRTH,
|
||||
ENABLE_SKILLS_BUILDER_PROFILE: process.env.ENABLE_SKILLS_BUILDER_PROFILE,
|
||||
ENABLE_NEW_PROFILE_VIEW: process.env.ENABLE_NEW_PROFILE_VIEW || null,
|
||||
}, 'App loadConfig override handler');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
@import "~@edx/frontend-component-footer/dist/footer";
|
||||
|
||||
@import './profile/index';
|
||||
@import 'profile/index';
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import React, { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const DateJoined = ({ date }) => {
|
||||
if (!date) { return null; }
|
||||
|
||||
return (
|
||||
<span className="small mb-0 text-gray-800">
|
||||
<FormattedMessage
|
||||
id="profile.datejoined.member.since"
|
||||
defaultMessage="Member since {year}"
|
||||
description="A label for how long the user has been a member"
|
||||
values={{
|
||||
year: <span className="font-weight-bold"> <FormattedDate value={new Date(date)} year="numeric" /> </span>,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
DateJoined.propTypes = {
|
||||
date: PropTypes.string,
|
||||
};
|
||||
DateJoined.defaultProps = {
|
||||
date: null,
|
||||
};
|
||||
|
||||
export default memo(DateJoined);
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const NotFoundPage = () => (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
<p className="my-0 py-5 text-muted max-width-32em">
|
||||
<FormattedMessage
|
||||
id="profile.notfound.message"
|
||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||
description="error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NotFoundPage;
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const PageLoading = ({ srMessage }) => (
|
||||
<div>
|
||||
<div className="d-flex justify-content-center align-items-center flex-column height-50vh">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
{srMessage && <span className="sr-only">{srMessage}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
PageLoading.propTypes = {
|
||||
srMessage: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PageLoading;
|
||||
@@ -1,463 +0,0 @@
|
||||
import React, {
|
||||
useEffect, useState, useContext, useCallback,
|
||||
} 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 } from '@edx/frontend-platform';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
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 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 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', 'ACCOUNT_SETTINGS_URL'], 'ProfilePage');
|
||||
|
||||
const ProfilePage = ({ params }) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const context = useContext(AppContext);
|
||||
const {
|
||||
dateJoined,
|
||||
courseCertificates,
|
||||
name,
|
||||
visibilityName,
|
||||
profileImage,
|
||||
savePhotoState,
|
||||
isLoadingProfile,
|
||||
photoUploadError,
|
||||
country,
|
||||
visibilityCountry,
|
||||
levelOfEducation,
|
||||
visibilityLevelOfEducation,
|
||||
socialLinks,
|
||||
draftSocialLinksByPlatform,
|
||||
visibilitySocialLinks,
|
||||
languageProficiencies,
|
||||
visibilityLanguageProficiencies,
|
||||
bio,
|
||||
visibilityBio,
|
||||
saveState,
|
||||
username,
|
||||
} = useSelector(profilePageSelector);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [viewMyRecordsUrl, setViewMyRecordsUrl] = useState(null);
|
||||
const isMobileView = useIsOnMobileScreen();
|
||||
const isTabletView = useIsOnTabletScreen();
|
||||
|
||||
useEffect(() => {
|
||||
const { CREDENTIALS_BASE_URL } = context.config;
|
||||
if (CREDENTIALS_BASE_URL) {
|
||||
setViewMyRecordsUrl(`${CREDENTIALS_BASE_URL}/records`);
|
||||
}
|
||||
|
||||
dispatch(fetchProfile(params.username));
|
||||
sendTrackingLogEvent('edx.profile.viewed', {
|
||||
username: params.username,
|
||||
});
|
||||
}, [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(authenticatedUserName, formData));
|
||||
}, [dispatch, authenticatedUserName]);
|
||||
|
||||
const handleDeleteProfilePhoto = useCallback(() => {
|
||||
dispatch(deleteProfilePhoto(authenticatedUserName));
|
||||
}, [dispatch, authenticatedUserName]);
|
||||
|
||||
const handleClose = useCallback((formId) => {
|
||||
dispatch(closeForm(formId));
|
||||
}, [dispatch]);
|
||||
|
||||
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));
|
||||
|
||||
const renderViewMyRecordsButton = () => {
|
||||
if (!(viewMyRecordsUrl && isAuthenticatedUserProfile())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Hyperlink
|
||||
className={classNames(
|
||||
'btn btn-brand bg-brand-500 btn-rounded font-weight-normal px-4 py-10px text-nowrap',
|
||||
{ 'w-100': isMobileView },
|
||||
)}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
destination={viewMyRecordsUrl}
|
||||
>
|
||||
{intl.formatMessage(messages['profile.viewMyRecords'])}
|
||||
</Hyperlink>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPhotoUploadErrorMessage = () => (
|
||||
photoUploadError && (
|
||||
<div className="row">
|
||||
<div className="col-md-4 col-lg-3">
|
||||
<Alert variant="danger" dismissible={false} show>
|
||||
{photoUploadError.userMessage}
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
const commonFormProps = {
|
||||
openHandler: handleOpen,
|
||||
closeHandler: handleClose,
|
||||
submitHandler: handleSubmit,
|
||||
changeHandler: handleChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="profile-page">
|
||||
{isLoadingProfile ? (
|
||||
<PageLoading srMessage={intl.formatMessage(messages['profile.loading'])} />
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'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={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',
|
||||
isMobileView || isTabletView ? 'flex-column' : 'flex-row',
|
||||
])}
|
||||
>
|
||||
<ProfileAvatar
|
||||
className="col p-0"
|
||||
src={profileImage.src}
|
||||
isDefault={profileImage.isDefault}
|
||||
onSave={handleSaveProfilePhoto}
|
||||
onDelete={handleDeleteProfilePhoto}
|
||||
savePhotoState={savePhotoState}
|
||||
isEditable={isAuthenticatedUserProfile()}
|
||||
/>
|
||||
<div
|
||||
className={classNames([
|
||||
'col h-100 w-100 m-0 p-0',
|
||||
isMobileView || isTabletView
|
||||
? 'd-flex flex-column justify-content-center align-items-center'
|
||||
: 'justify-content-start align-items-start',
|
||||
])}
|
||||
>
|
||||
<p className="row m-0 font-weight-bold text-truncate text-primary-500 h3">
|
||||
{params.username}
|
||||
</p>
|
||||
{isBlockVisible(name) && (
|
||||
<p className="row pt-2 text-gray-800 font-weight-normal m-0 p">
|
||||
{name}
|
||||
</p>
|
||||
)}
|
||||
<div className={classNames(
|
||||
'row pt-2 m-0',
|
||||
isMobileView
|
||||
? 'd-flex justify-content-center align-items-center flex-column'
|
||||
: 'g-1rem',
|
||||
)}
|
||||
>
|
||||
<DateJoined date={dateJoined} />
|
||||
<UserCertificateSummary count={courseCertificates?.length || 0} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames([
|
||||
'p-0 ',
|
||||
isMobileView || isTabletView ? 'col d-flex justify-content-center' : 'col-auto',
|
||||
])}
|
||||
>
|
||||
{renderViewMyRecordsButton()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
{renderPhotoUploadErrorMessage()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -1,26 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'profile.viewMyRecords': {
|
||||
id: 'profile.viewMyRecords',
|
||||
defaultMessage: 'View My Records',
|
||||
description: 'A link to go view my academic records',
|
||||
},
|
||||
'profile.loading': {
|
||||
id: 'profile.loading',
|
||||
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;
|
||||
@@ -1,259 +0,0 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import * as analytics from '@edx/frontend-platform/analytics';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
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]);
|
||||
|
||||
const storeMocks = {
|
||||
loadingApp,
|
||||
viewOwnProfile,
|
||||
viewOtherProfile,
|
||||
invalidUser,
|
||||
};
|
||||
|
||||
const requiredProfilePageProps = {
|
||||
params: { username: 'staff' },
|
||||
};
|
||||
|
||||
Object.defineProperty(global.document, 'cookie', {
|
||||
writable: true,
|
||||
value: `${getConfig().LANGUAGE_PREFERENCE_COOKIE_NAME}=en`,
|
||||
});
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
configure: () => {},
|
||||
getAuthenticatedUser: () => null,
|
||||
fetchAuthenticatedUser: () => null,
|
||||
getAuthenticatedHttpClient: jest.fn(),
|
||||
AUTHENTICATED_USER_CHANGED: 'user_changed',
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
configure: () => {},
|
||||
identifyAnonymousUser: jest.fn(),
|
||||
identifyAuthenticatedUser: jest.fn(),
|
||||
sendTrackingLogEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
configureI18n({
|
||||
loggingService: { logError: jest.fn() },
|
||||
config: {
|
||||
ENVIRONMENT: 'production',
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
|
||||
},
|
||||
messages,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
analytics.sendTrackingLogEvent.mockReset();
|
||||
useNavigate.mockReset();
|
||||
});
|
||||
|
||||
const ProfilePageWrapper = ({
|
||||
contextValue, store, params,
|
||||
}) => (
|
||||
<AppContext.Provider value={contextValue}>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<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({
|
||||
username: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
describe('<ProfilePage />', () => {
|
||||
describe('Renders correctly in various states', () => {
|
||||
it('app loading', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: null, username: null, administrator: false },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.loadingApp)}
|
||||
/>
|
||||
);
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('viewing own profile', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.viewOwnProfile)}
|
||||
/>
|
||||
);
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('viewing other profile with all fields', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore({
|
||||
...storeMocks.viewOtherProfile,
|
||||
profilePage: {
|
||||
...storeMocks.viewOtherProfile.profilePage,
|
||||
account: {
|
||||
...storeMocks.viewOtherProfile.profilePage.account,
|
||||
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',
|
||||
},
|
||||
},
|
||||
})}
|
||||
params={{ username: 'verified' }}
|
||||
/>
|
||||
);
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('without credentials service', () => {
|
||||
const config = getConfig();
|
||||
config.CREDENTIALS_BASE_URL = '';
|
||||
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.viewOwnProfile)}
|
||||
/>
|
||||
);
|
||||
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', () => {
|
||||
it('calls sendTrackingLogEvent when mounting', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
render(
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.loadingApp)}
|
||||
params={{ username: 'test-username' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
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'
|
||||
}
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
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: null,
|
||||
savePhotoState: null,
|
||||
currentlyEditingField: null,
|
||||
account: {
|
||||
username: 'staff',
|
||||
socialLinks: []
|
||||
},
|
||||
preferences: {},
|
||||
courseCertificates: [],
|
||||
drafts: {},
|
||||
isLoadingProfile: true,
|
||||
isAuthenticatedUserProfile: true,
|
||||
countriesCodesList: ['US', 'CA', 'GB', 'ME']
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
pathname: '/u/staff',
|
||||
search: '',
|
||||
hash: ''
|
||||
},
|
||||
action: 'POP'
|
||||
}
|
||||
};
|
||||
@@ -1,139 +0,0 @@
|
||||
module.exports = {
|
||||
userAccount: {
|
||||
loading: false,
|
||||
error: null,
|
||||
username: 'staff',
|
||||
email: 'staff@example.com',
|
||||
bio: 'This is my bio',
|
||||
name: 'Lemon Seltzer',
|
||||
country: 'ME',
|
||||
socialLinks: [
|
||||
{
|
||||
platform: 'facebook',
|
||||
socialLink: 'https://www.facebook.com/aloha'
|
||||
},
|
||||
{
|
||||
platform: 'twitter',
|
||||
socialLink: 'https://www.twitter.com/ALOHA'
|
||||
}
|
||||
],
|
||||
profileImage: {
|
||||
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
|
||||
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
|
||||
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
|
||||
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
|
||||
hasImage: true
|
||||
},
|
||||
levelOfEducation: 'el',
|
||||
mailingAddress: null,
|
||||
extendedProfile: [],
|
||||
dateJoined: '2017-06-07T00:44:23Z',
|
||||
accomplishmentsShared: false,
|
||||
isActive: true,
|
||||
yearOfBirth: 1901,
|
||||
goals: null,
|
||||
languageProficiencies: [
|
||||
{
|
||||
code: 'yo'
|
||||
}
|
||||
],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: false,
|
||||
secondaryEmail: null,
|
||||
timeZone: null,
|
||||
gender: null,
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: null,
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
saveState: 'pending',
|
||||
savePhotoState: null,
|
||||
currentlyEditingField: 'bio',
|
||||
isAuthenticatedUserProfile: true,
|
||||
account: {
|
||||
mailingAddress: null,
|
||||
profileImage: {
|
||||
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
|
||||
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
|
||||
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
|
||||
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
|
||||
hasImage: true
|
||||
},
|
||||
extendedProfile: [],
|
||||
dateJoined: '2017-06-07T00:44:23Z',
|
||||
accomplishmentsShared: false,
|
||||
email: 'staff@example.com',
|
||||
username: 'staff',
|
||||
bio: 'This is my bio',
|
||||
isActive: true,
|
||||
yearOfBirth: 1901,
|
||||
goals: null,
|
||||
languageProficiencies: [
|
||||
{
|
||||
code: 'yo'
|
||||
}
|
||||
],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: false,
|
||||
name: 'Lemon Seltzer',
|
||||
secondaryEmail: null,
|
||||
country: 'ME',
|
||||
socialLinks: [
|
||||
{
|
||||
platform: 'facebook',
|
||||
socialLink: 'https://www.facebook.com/aloha'
|
||||
},
|
||||
{
|
||||
platform: 'twitter',
|
||||
socialLink: 'https://www.twitter.com/ALOHA'
|
||||
}
|
||||
],
|
||||
timeZone: null,
|
||||
levelOfEducation: 'el',
|
||||
gender: null,
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: null,
|
||||
},
|
||||
preferences: {
|
||||
visibilityUserLocation: 'all_users',
|
||||
visibilitySocialLinks: 'all_users',
|
||||
visibilityCertificates: 'private',
|
||||
visibilityLevelOfEducation: 'private',
|
||||
visibilityCourseCertificates: 'all_users',
|
||||
prefLang: 'en',
|
||||
visibilityBio: 'all_users',
|
||||
visibilityName: 'private',
|
||||
visibilityLanguageProficiencies: 'all_users',
|
||||
visibilityCountry: 'all_users',
|
||||
accountPrivacy: 'custom',
|
||||
visibilityLearningGoal: 'private',
|
||||
},
|
||||
courseCertificates: [
|
||||
{
|
||||
username: 'staff',
|
||||
status: 'downloadable',
|
||||
courseDisplayName: 'edX Demonstration Course',
|
||||
grade: '0.89',
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
courseOrganization: 'edX',
|
||||
modifiedDate: '2019-03-04T19:31:39.930255Z',
|
||||
isPassing: true,
|
||||
downloadUrl: 'http://www.example.com/',
|
||||
certificateType: 'verified',
|
||||
createdDate: '2019-03-04T19:31:39.896806Z'
|
||||
}
|
||||
],
|
||||
drafts: {},
|
||||
isLoadingProfile: false,
|
||||
disabledCountries: [],
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
pathname: '/u/staff',
|
||||
search: '',
|
||||
hash: ''
|
||||
},
|
||||
action: 'POP'
|
||||
}
|
||||
};
|
||||
@@ -1,105 +0,0 @@
|
||||
module.exports = {
|
||||
userAccount: {
|
||||
loading: false,
|
||||
error: null,
|
||||
username: 'staff',
|
||||
email: 'staff@example.com',
|
||||
bio: 'This is my bio',
|
||||
name: 'Lemon Seltzer',
|
||||
country: 'ME',
|
||||
socialLinks: [
|
||||
{
|
||||
platform: 'facebook',
|
||||
socialLink: 'https://www.facebook.com/aloha'
|
||||
},
|
||||
{
|
||||
platform: 'twitter',
|
||||
socialLink: 'https://www.twitter.com/ALOHA'
|
||||
}
|
||||
],
|
||||
profileImage: {
|
||||
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
|
||||
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
|
||||
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
|
||||
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
|
||||
hasImage: true
|
||||
},
|
||||
levelOfEducation: 'el',
|
||||
mailingAddress: null,
|
||||
extendedProfile: [],
|
||||
dateJoined: '2017-06-07T00:44:23Z',
|
||||
accomplishmentsShared: false,
|
||||
isActive: true,
|
||||
yearOfBirth: 1901,
|
||||
goals: null,
|
||||
languageProficiencies: [
|
||||
{
|
||||
code: 'yo'
|
||||
}
|
||||
],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: false,
|
||||
secondaryEmail: null,
|
||||
timeZone: null,
|
||||
gender: null,
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: 'advance_career',
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
saveState: null,
|
||||
savePhotoState: null,
|
||||
currentlyEditingField: null,
|
||||
isAuthenticatedUserProfile: false,
|
||||
account: {
|
||||
mailingAddress: null,
|
||||
profileImage: {
|
||||
imageUrlFull: 'http://localhost:18000/static/images/profiles/default_500.png',
|
||||
imageUrlLarge: 'http://localhost:18000/static/images/profiles/default_120.png',
|
||||
imageUrlMedium: 'http://localhost:18000/static/images/profiles/default_50.png',
|
||||
imageUrlSmall: 'http://localhost:18000/static/images/profiles/default_30.png',
|
||||
hasImage: false
|
||||
},
|
||||
extendedProfile: [],
|
||||
dateJoined: '2017-06-07T00:44:19Z',
|
||||
accomplishmentsShared: false,
|
||||
email: 'verified@example.com',
|
||||
username: 'verified',
|
||||
bio: null,
|
||||
isActive: true,
|
||||
yearOfBirth: null,
|
||||
goals: null,
|
||||
languageProficiencies: [],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: true,
|
||||
name: '',
|
||||
secondaryEmail: null,
|
||||
country: null,
|
||||
socialLinks: [],
|
||||
timeZone: null,
|
||||
levelOfEducation: null,
|
||||
gender: null,
|
||||
accountPrivacy: 'private'
|
||||
},
|
||||
preferences: {
|
||||
visibilityName: 'all_users',
|
||||
visibilityCountry: 'all_users',
|
||||
visibilityLevelOfEducation: 'all_users',
|
||||
visibilityLanguageProficiencies: 'all_users',
|
||||
visibilitySocialLinks: 'all_users',
|
||||
visibilityBio: 'all_users'
|
||||
},
|
||||
courseCertificates: [],
|
||||
drafts: {},
|
||||
isLoadingProfile: false,
|
||||
countriesCodesList: ['US', 'CA', 'GB', 'ME']
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
pathname: '/u/verified',
|
||||
search: '',
|
||||
hash: ''
|
||||
},
|
||||
action: 'POP'
|
||||
}
|
||||
};
|
||||
@@ -1,139 +0,0 @@
|
||||
module.exports = {
|
||||
userAccount: {
|
||||
loading: false,
|
||||
error: null,
|
||||
username: 'staff',
|
||||
email: 'staff@example.com',
|
||||
bio: 'This is my bio',
|
||||
name: 'Lemon Seltzer',
|
||||
country: 'ME',
|
||||
socialLinks: [
|
||||
{
|
||||
platform: 'facebook',
|
||||
socialLink: 'https://www.facebook.com/aloha'
|
||||
},
|
||||
{
|
||||
platform: 'twitter',
|
||||
socialLink: 'https://www.twitter.com/ALOHA'
|
||||
}
|
||||
],
|
||||
profileImage: {
|
||||
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
|
||||
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
|
||||
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
|
||||
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
|
||||
hasImage: true
|
||||
},
|
||||
levelOfEducation: 'el',
|
||||
mailingAddress: null,
|
||||
extendedProfile: [],
|
||||
dateJoined: '2017-06-07T00:44:23Z',
|
||||
accomplishmentsShared: false,
|
||||
isActive: true,
|
||||
yearOfBirth: 1901,
|
||||
goals: null,
|
||||
languageProficiencies: [
|
||||
{
|
||||
code: 'yo'
|
||||
}
|
||||
],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: false,
|
||||
secondaryEmail: null,
|
||||
timeZone: null,
|
||||
gender: null,
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: 'advance_career'
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
saveState: null,
|
||||
savePhotoState: null,
|
||||
currentlyEditingField: null,
|
||||
isAuthenticatedUserProfile: true,
|
||||
account: {
|
||||
mailingAddress: null,
|
||||
profileImage: {
|
||||
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
|
||||
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
|
||||
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
|
||||
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
|
||||
hasImage: true
|
||||
},
|
||||
extendedProfile: [],
|
||||
dateJoined: '2017-06-07T00:44:23Z',
|
||||
accomplishmentsShared: false,
|
||||
email: 'staff@example.com',
|
||||
username: 'staff',
|
||||
bio: 'This is my bio',
|
||||
isActive: true,
|
||||
yearOfBirth: 1901,
|
||||
goals: null,
|
||||
languageProficiencies: [
|
||||
{
|
||||
code: 'yo'
|
||||
}
|
||||
],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: false,
|
||||
name: 'Lemon Seltzer',
|
||||
secondaryEmail: null,
|
||||
country: 'ME',
|
||||
socialLinks: [
|
||||
{
|
||||
platform: 'facebook',
|
||||
socialLink: 'https://www.facebook.com/aloha'
|
||||
},
|
||||
{
|
||||
platform: 'twitter',
|
||||
socialLink: 'https://www.twitter.com/ALOHA'
|
||||
}
|
||||
],
|
||||
timeZone: null,
|
||||
levelOfEducation: 'el',
|
||||
gender: null,
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: 'advance_career'
|
||||
},
|
||||
preferences: {
|
||||
visibilityUserLocation: 'all_users',
|
||||
visibilitySocialLinks: 'all_users',
|
||||
visibilityCertificates: 'private',
|
||||
visibilityLevelOfEducation: 'private',
|
||||
visibilityCourseCertificates: 'all_users',
|
||||
prefLang: 'en',
|
||||
visibilityBio: 'all_users',
|
||||
visibilityName: 'private',
|
||||
visibilityLanguageProficiencies: 'all_users',
|
||||
visibilityCountry: 'all_users',
|
||||
accountPrivacy: 'custom',
|
||||
visibilityLearningGoal: 'private',
|
||||
},
|
||||
courseCertificates: [
|
||||
{
|
||||
username: 'staff',
|
||||
status: 'downloadable',
|
||||
courseDisplayName: 'edX Demonstration Course',
|
||||
grade: '0.89',
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
courseOrganization: 'edX',
|
||||
modifiedDate: '2019-03-04T19:31:39.930255Z',
|
||||
isPassing: true,
|
||||
downloadUrl: 'http://www.example.com/',
|
||||
certificateType: 'verified',
|
||||
createdDate: '2019-03-04T19:31:39.896806Z'
|
||||
}
|
||||
],
|
||||
drafts: {},
|
||||
isLoadingProfile: false,
|
||||
countriesCodesList: ['US', 'CA', 'GB', 'ME']
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
pathname: '/u/staff',
|
||||
search: '',
|
||||
hash: ''
|
||||
},
|
||||
action: 'POP'
|
||||
}
|
||||
};
|
||||
@@ -1,2019 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ProfilePage /> Renders correctly in various states app loading 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="profile-page"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="d-flex justify-content-center align-items-center flex-column height-50vh"
|
||||
>
|
||||
<div
|
||||
class="spinner-border text-primary"
|
||||
role="status"
|
||||
>
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
Profile loading...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 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"
|
||||
>
|
||||
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"
|
||||
>
|
||||
Verified User
|
||||
</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"
|
||||
>
|
||||
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>
|
||||
`;
|
||||
|
||||
exports[`<ProfilePage /> Renders correctly in various states viewing own profile 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"
|
||||
>
|
||||
<img
|
||||
alt="profile avatar"
|
||||
class="w-100 h-100 d-block rounded-circle overflow-hidden object-fit-cover"
|
||||
data-hj-suppress="true"
|
||||
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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
staff
|
||||
</p>
|
||||
<p
|
||||
class="row pt-2 text-gray-800 font-weight-normal m-0 p"
|
||||
>
|
||||
Lemon Seltzer
|
||||
</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>
|
||||
<span
|
||||
class="small m-0 text-gray-800"
|
||||
>
|
||||
<span
|
||||
class="font-weight-bold"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
certifications
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="p-0 col-auto"
|
||||
>
|
||||
<a
|
||||
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"
|
||||
>
|
||||
View My Records
|
||||
</a>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
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-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 and profile image are visible to others on localhost.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col"
|
||||
>
|
||||
<div
|
||||
class="row align-items-center pt-5 g-3rem"
|
||||
>
|
||||
<div
|
||||
class="col-auto d-flex align-items-center p-0"
|
||||
>
|
||||
<div
|
||||
class="col certificate p-4 border-light-400 bg-light-200 w-100 h-100"
|
||||
>
|
||||
<div
|
||||
class="certificate-type-illustration"
|
||||
style="background-image: url(icon/mock/path);"
|
||||
/>
|
||||
<div
|
||||
class="d-flex flex-column position-relative p-0 width-314px"
|
||||
>
|
||||
<div
|
||||
class="w-100 color-black"
|
||||
>
|
||||
<p
|
||||
class="mb-0 font-weight-normal small"
|
||||
>
|
||||
Verified Certificate
|
||||
</p>
|
||||
<p
|
||||
class="m-0 color-black h4"
|
||||
>
|
||||
edX Demonstration Course
|
||||
</p>
|
||||
<p
|
||||
class="mb-0 small"
|
||||
>
|
||||
From
|
||||
</p>
|
||||
<h5
|
||||
class="mb-0 color-black"
|
||||
>
|
||||
edX
|
||||
</h5>
|
||||
<p
|
||||
class="mb-0 small"
|
||||
>
|
||||
Completed on
|
||||
3/4/2019
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="pt-3"
|
||||
>
|
||||
<a
|
||||
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"
|
||||
>
|
||||
View Certificate
|
||||
</a>
|
||||
</div>
|
||||
<p
|
||||
class="mb-0 pt-3 small"
|
||||
>
|
||||
Credential ID
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<ProfilePage /> Renders correctly in various states without credentials service 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"
|
||||
>
|
||||
<img
|
||||
alt="profile avatar"
|
||||
class="w-100 h-100 d-block rounded-circle overflow-hidden object-fit-cover"
|
||||
data-hj-suppress="true"
|
||||
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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
staff
|
||||
</p>
|
||||
<p
|
||||
class="row pt-2 text-gray-800 font-weight-normal m-0 p"
|
||||
>
|
||||
Lemon Seltzer
|
||||
</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>
|
||||
<span
|
||||
class="small m-0 text-gray-800"
|
||||
>
|
||||
<span
|
||||
class="font-weight-bold"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
certifications
|
||||
</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"
|
||||
>
|
||||
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-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 and profile image are visible to others on localhost.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col"
|
||||
>
|
||||
<div
|
||||
class="row align-items-center pt-5 g-3rem"
|
||||
>
|
||||
<div
|
||||
class="col-auto d-flex align-items-center p-0"
|
||||
>
|
||||
<div
|
||||
class="col certificate p-4 border-light-400 bg-light-200 w-100 h-100"
|
||||
>
|
||||
<div
|
||||
class="certificate-type-illustration"
|
||||
style="background-image: url(icon/mock/path);"
|
||||
/>
|
||||
<div
|
||||
class="d-flex flex-column position-relative p-0 width-314px"
|
||||
>
|
||||
<div
|
||||
class="w-100 color-black"
|
||||
>
|
||||
<p
|
||||
class="mb-0 font-weight-normal small"
|
||||
>
|
||||
Verified Certificate
|
||||
</p>
|
||||
<p
|
||||
class="m-0 color-black h4"
|
||||
>
|
||||
edX Demonstration Course
|
||||
</p>
|
||||
<p
|
||||
class="mb-0 small"
|
||||
>
|
||||
From
|
||||
</p>
|
||||
<h5
|
||||
class="mb-0 color-black"
|
||||
>
|
||||
edX
|
||||
</h5>
|
||||
<p
|
||||
class="mb-0 small"
|
||||
>
|
||||
Completed on
|
||||
3/4/2019
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="pt-3"
|
||||
>
|
||||
<a
|
||||
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"
|
||||
>
|
||||
View Certificate
|
||||
</a>
|
||||
</div>
|
||||
<p
|
||||
class="mb-0 pt-3 small"
|
||||
>
|
||||
Credential ID
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
|
||||
<title>avatar</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="avatar" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M4.10255106,18.1351061 C4.7170266,16.0581859 8.01891846,14.4720277 12,14.4720277 C15.9810815,14.4720277 19.2829734,16.0581859 19.8974489,18.1351061 C21.215206,16.4412566 22,14.3122775 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,14.3122775 2.78479405,16.4412566 4.10255106,18.1351061 Z M12,24 C5.372583,24 0,18.627417 0,12 C0,5.372583 5.372583,0 12,0 C18.627417,0 24,5.372583 24,12 C24,18.627417 18.627417,24 12,24 Z M12,13 C9.790861,13 8,11.209139 8,9 C8,6.790861 9.790861,5 12,5 C14.209139,5 16,6.790861 16,9 C16,11.209139 14.209139,13 12,13 Z" fill="currentColor"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1006 B |
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="110px" height="100px" viewBox="0 0 110 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
|
||||
<title>micro-masters</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="micro-masters" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<polygon id="Fill-14" fill="#005585" points="1.00481933 99 1 96.14832 108.995272 96 109 98.85168"></polygon>
|
||||
<polygon id="Fill-16" fill="#005585" points="1.00481933 54 1 51.14832 108.995272 51 109 53.85168"></polygon>
|
||||
<polygon id="Fill-18" fill="#005585" points="24.9998189 88 4.22751315 66.9102771 4.22751315 87.0451102 1 87.0451102 1 59 24.9998189 83.3666894 49 59 49 87.0451102 45.7724869 87.0451102 45.7724869 66.9102771"></polygon>
|
||||
<polygon id="Fill-20" fill="#005585" points="83.0003622 88 62.2275375 66.9102771 62.2275375 87.0451102 59 87.0451102 59 59 83.0003622 83.3666894 107 59 107 87.0451102 103.772372 87.0451102 103.772372 66.9102771"></polygon>
|
||||
<path d="M46.8337141,5.73372796 C40.9959838,9.0009662 32.7599012,14.7887333 26.6032377,23.8303431 C22.1430738,22.980271 18.7577893,22.0941759 16.6724707,21.4890245 C24.4296997,13.0587208 35.0333668,7.38581116 46.8337141,5.73372796 L46.8337141,5.73372796 Z M52.9498983,26.3252576 C44.0609738,26.2888724 36.3049246,25.4607944 30.1183101,24.4518768 C37.8086485,13.8504137 48.4460785,8.26376038 52.9498983,6.25307565 L52.9498983,26.3252576 Z M56.1856092,6.53148511 C60.9824046,8.72165163 70.9689895,14.1856636 78.3079931,24.2371866 C70.4533778,25.5777336 63.0182586,26.1827945 56.1856092,26.3036256 L56.1856092,6.53148511 Z M91.2530147,21.4114572 C88.0536989,22.2655118 84.9019418,22.9909512 81.8101774,23.5971888 C75.749448,14.7672824 67.7047798,9.0930151 61.9038076,5.84261176 C73.3677956,7.61959908 83.6636931,13.2007313 91.2530147,21.4114572 L91.2530147,21.4114572 Z M5.35796556,42 C7.1655563,35.3542865 10.2683027,29.3264862 14.378947,24.1676747 C16.125002,24.7123653 19.7105047,25.7399279 24.7732291,26.7529184 C22.2125965,31.1374149 20.1687556,36.1942214 19.0530339,42 L22.3555447,42 C23.5090228,36.4059248 25.5817257,31.5545765 28.1556093,27.3837747 C34.7214222,28.5242036 43.1689773,29.5005374 52.9498983,29.5419006 L52.9498983,42 L56.1856092,42 L56.1856092,29.5223504 C63.6041191,29.395636 71.7140444,28.7101116 80.2902074,27.16121 C82.9053872,31.3760904 85.0043199,36.3025621 86.1572534,42 L89.460218,42 C88.3437702,36.0858806 86.2630805,30.9538601 83.6533464,26.5180443 C86.9031251,25.8529752 90.216981,25.0530458 93.5773971,24.1181656 C97.7090979,29.2882909 100.82809,35.3331976 102.641853,42 L106,42 C100.00834,18.4385583 78.6589648,2 53.9991832,2 C29.3299624,2 7.9913882,18.4352094 2,42 L5.35796556,42 Z" id="Fill-22" fill="#005585"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 13 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,137 +0,0 @@
|
||||
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');
|
||||
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,
|
||||
payload: { username },
|
||||
});
|
||||
|
||||
export const fetchProfileBegin = () => ({
|
||||
type: FETCH_PROFILE.BEGIN,
|
||||
});
|
||||
|
||||
export const fetchProfileSuccess = (
|
||||
account,
|
||||
preferences,
|
||||
courseCertificates,
|
||||
isAuthenticatedUserProfile,
|
||||
) => ({
|
||||
type: FETCH_PROFILE.SUCCESS,
|
||||
account,
|
||||
preferences,
|
||||
courseCertificates,
|
||||
isAuthenticatedUserProfile,
|
||||
});
|
||||
|
||||
export const fetchProfileReset = () => ({
|
||||
type: FETCH_PROFILE.RESET,
|
||||
});
|
||||
|
||||
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,
|
||||
payload: {
|
||||
username,
|
||||
formData,
|
||||
},
|
||||
});
|
||||
|
||||
export const saveProfilePhotoBegin = () => ({
|
||||
type: SAVE_PROFILE_PHOTO.BEGIN,
|
||||
});
|
||||
|
||||
export const saveProfilePhotoSuccess = profileImage => ({
|
||||
type: SAVE_PROFILE_PHOTO.SUCCESS,
|
||||
payload: { profileImage },
|
||||
});
|
||||
|
||||
export const saveProfilePhotoReset = () => ({
|
||||
type: SAVE_PROFILE_PHOTO.RESET,
|
||||
});
|
||||
|
||||
export const saveProfilePhotoFailure = error => ({
|
||||
type: SAVE_PROFILE_PHOTO.FAILURE,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
export const deleteProfilePhoto = username => ({
|
||||
type: DELETE_PROFILE_PHOTO.BASE,
|
||||
payload: {
|
||||
username,
|
||||
},
|
||||
});
|
||||
|
||||
export const deleteProfilePhotoBegin = () => ({
|
||||
type: DELETE_PROFILE_PHOTO.BEGIN,
|
||||
});
|
||||
|
||||
export const deleteProfilePhotoSuccess = profileImage => ({
|
||||
type: DELETE_PROFILE_PHOTO.SUCCESS,
|
||||
payload: { 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,
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
import {
|
||||
SAVE_PROFILE_PHOTO,
|
||||
saveProfilePhotoBegin,
|
||||
saveProfilePhotoSuccess,
|
||||
saveProfilePhotoFailure,
|
||||
saveProfilePhotoReset,
|
||||
saveProfilePhoto,
|
||||
DELETE_PROFILE_PHOTO,
|
||||
deleteProfilePhotoBegin,
|
||||
deleteProfilePhotoSuccess,
|
||||
deleteProfilePhotoReset,
|
||||
deleteProfilePhoto,
|
||||
} from './actions';
|
||||
|
||||
describe('SAVE profile photo actions', () => {
|
||||
it('should create an action to signal the start of a profile photo save', () => {
|
||||
const formData = 'multipart form data';
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE_PHOTO.BASE,
|
||||
payload: {
|
||||
username: 'myusername',
|
||||
formData,
|
||||
},
|
||||
};
|
||||
expect(saveProfilePhoto('myusername', formData)).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo save beginning', () => {
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE_PHOTO.BEGIN,
|
||||
};
|
||||
expect(saveProfilePhotoBegin()).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo save success', () => {
|
||||
const newPhotoData = { hasImage: true };
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE_PHOTO.SUCCESS,
|
||||
payload: {
|
||||
profileImage: newPhotoData,
|
||||
},
|
||||
};
|
||||
expect(saveProfilePhotoSuccess(newPhotoData)).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo save reset', () => {
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE_PHOTO.RESET,
|
||||
};
|
||||
expect(saveProfilePhotoReset()).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo save failure', () => {
|
||||
const error = 'Test failure';
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE_PHOTO.FAILURE,
|
||||
payload: { error },
|
||||
};
|
||||
expect(saveProfilePhotoFailure(error)).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE profile photo actions', () => {
|
||||
it('should create an action to signal the start of a profile photo deletion', () => {
|
||||
const expectedAction = {
|
||||
type: DELETE_PROFILE_PHOTO.BASE,
|
||||
payload: {
|
||||
username: 'myusername',
|
||||
},
|
||||
};
|
||||
expect(deleteProfilePhoto('myusername')).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo deletion beginning', () => {
|
||||
const expectedAction = {
|
||||
type: DELETE_PROFILE_PHOTO.BEGIN,
|
||||
};
|
||||
expect(deleteProfilePhotoBegin()).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo deletion success', () => {
|
||||
const defaultPhotoData = { hasImage: false };
|
||||
const expectedAction = {
|
||||
type: DELETE_PROFILE_PHOTO.SUCCESS,
|
||||
payload: {
|
||||
profileImage: defaultPhotoData,
|
||||
},
|
||||
};
|
||||
expect(deleteProfilePhotoSuccess(defaultPhotoData)).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo deletion reset', () => {
|
||||
const expectedAction = {
|
||||
type: DELETE_PROFILE_PHOTO.RESET,
|
||||
};
|
||||
expect(deleteProfilePhotoReset()).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
const EDUCATION_LEVELS = [
|
||||
'p',
|
||||
'm',
|
||||
'b',
|
||||
'a',
|
||||
'hs',
|
||||
'jhs',
|
||||
'el',
|
||||
'none',
|
||||
'other',
|
||||
];
|
||||
|
||||
const SOCIAL = {
|
||||
linkedin: {
|
||||
title: 'LinkedIn',
|
||||
},
|
||||
twitter: {
|
||||
title: 'Twitter',
|
||||
},
|
||||
facebook: {
|
||||
title: 'Facebook',
|
||||
},
|
||||
};
|
||||
|
||||
const FIELD_LABELS = {
|
||||
COUNTRY: 'country',
|
||||
};
|
||||
|
||||
export {
|
||||
EDUCATION_LEVELS,
|
||||
SOCIAL,
|
||||
FIELD_LABELS,
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
const mockData = {
|
||||
learningGoal: 'advance_career',
|
||||
editMode: 'static',
|
||||
visibilityLearningGoal: 'private',
|
||||
};
|
||||
|
||||
export default mockData;
|
||||
@@ -1,84 +0,0 @@
|
||||
// This test file simply creates a contract that defines
|
||||
// expectations and correct responses from the Pact stub server.
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
|
||||
|
||||
import { initializeMockApp, getConfig, setConfig } from '@edx/frontend-platform';
|
||||
import { getAccount } from './services';
|
||||
|
||||
const expectedUserInfo200 = {
|
||||
username: 'staff',
|
||||
email: 'staff@example.com',
|
||||
bio: 'This is my bio',
|
||||
name: 'Lemon Seltzer',
|
||||
country: 'ME',
|
||||
dateJoined: '2017-06-07T00:44:23Z',
|
||||
isActive: true,
|
||||
yearOfBirth: 1901,
|
||||
languageProficiencies: [],
|
||||
levelOfEducation: null,
|
||||
profileImage: {},
|
||||
socialLinks: [],
|
||||
};
|
||||
|
||||
const provider = new PactV3({
|
||||
log: path.resolve(process.cwd(), 'src/pact-logs/pact.log'),
|
||||
dir: path.resolve(process.cwd(), 'src/pacts'),
|
||||
consumer: 'frontend-app-profile',
|
||||
provider: 'edx-platform',
|
||||
});
|
||||
|
||||
describe('getAccount for one username', () => {
|
||||
beforeAll(async () => {
|
||||
initializeMockApp();
|
||||
});
|
||||
it('returns a HTTP 200 and user information', async () => {
|
||||
const username200 = 'staff';
|
||||
await provider.addInteraction({
|
||||
states: [{ description: "I have a user's basic information" }],
|
||||
uponReceiving: "A request for user's basic information",
|
||||
withRequest: {
|
||||
method: 'GET',
|
||||
path: `/api/user/v1/accounts/${username200}`,
|
||||
headers: {},
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
headers: {},
|
||||
body: MatchersV3.like(expectedUserInfo200),
|
||||
},
|
||||
});
|
||||
return provider.executeTest(async (mockserver) => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
LMS_BASE_URL: mockserver.url,
|
||||
});
|
||||
const response = await getAccount(username200);
|
||||
expect(response).toEqual(expectedUserInfo200);
|
||||
});
|
||||
});
|
||||
|
||||
it('Account does not exist', async () => {
|
||||
const username404 = 'staff_not_found';
|
||||
await provider.addInteraction({
|
||||
states: [{ description: "Account and user's information does not exist" }],
|
||||
uponReceiving: "A request for user's basic information",
|
||||
withRequest: {
|
||||
method: 'GET',
|
||||
path: `/api/user/v1/accounts/${username404}`,
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 404,
|
||||
},
|
||||
});
|
||||
await provider.executeTest(async (mockserver) => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
LMS_BASE_URL: mockserver.url,
|
||||
});
|
||||
await expect(getAccount(username404).then((response) => response.data)).rejects.toThrow('Request failed with status code 404');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,181 +0,0 @@
|
||||
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: '',
|
||||
},
|
||||
courseCertificates: [],
|
||||
drafts: {},
|
||||
isLoadingProfile: true,
|
||||
isAuthenticatedUserProfile: false,
|
||||
disabledCountries: ['RU'],
|
||||
countriesCodesList: [],
|
||||
};
|
||||
|
||||
const profilePage = (state = initialState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case FETCH_PROFILE.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
// TODO: uncomment this line after ARCH-438 Image Post API returns the url
|
||||
// is complete. Right now we refetch the whole profile causing us to show a full reload
|
||||
// instead of a partial one.
|
||||
// isLoadingProfile: true,
|
||||
};
|
||||
case FETCH_PROFILE.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
account: {
|
||||
...state.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 || [],
|
||||
};
|
||||
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,
|
||||
savePhotoState: 'pending',
|
||||
errors: {},
|
||||
};
|
||||
case SAVE_PROFILE_PHOTO.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
account: { ...state.account, profileImage: action.payload.profileImage },
|
||||
savePhotoState: 'complete',
|
||||
errors: {},
|
||||
};
|
||||
case SAVE_PROFILE_PHOTO.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: 'error',
|
||||
errors: { ...state.errors, photo: action.payload.error },
|
||||
};
|
||||
case SAVE_PROFILE_PHOTO.RESET:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: null,
|
||||
errors: {},
|
||||
};
|
||||
case DELETE_PROFILE_PHOTO.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: 'pending',
|
||||
errors: {},
|
||||
};
|
||||
case DELETE_PROFILE_PHOTO.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
account: { ...state.account, profileImage: action.payload.profileImage },
|
||||
savePhotoState: 'complete',
|
||||
errors: {},
|
||||
};
|
||||
case DELETE_PROFILE_PHOTO.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: 'error',
|
||||
errors: { ...state.errors, ...action.payload.errors },
|
||||
};
|
||||
case DELETE_PROFILE_PHOTO.RESET:
|
||||
return {
|
||||
...state,
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
export default profilePage;
|
||||
@@ -1,191 +0,0 @@
|
||||
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,
|
||||
DELETE_PROFILE_PHOTO,
|
||||
fetchProfileBegin,
|
||||
fetchProfileReset,
|
||||
fetchProfileSuccess,
|
||||
FETCH_PROFILE,
|
||||
resetDrafts,
|
||||
saveProfileBegin,
|
||||
saveProfileFailure,
|
||||
saveProfileReset,
|
||||
saveProfileSuccess,
|
||||
SAVE_PROFILE,
|
||||
saveProfilePhotoBegin,
|
||||
saveProfilePhotoReset,
|
||||
saveProfilePhotoSuccess,
|
||||
SAVE_PROFILE_PHOTO,
|
||||
} from './actions';
|
||||
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;
|
||||
let preferences = {};
|
||||
let account = userAccount;
|
||||
let courseCertificates = null;
|
||||
let countriesCodesList = [];
|
||||
|
||||
try {
|
||||
yield put(fetchProfileBegin());
|
||||
|
||||
const calls = [
|
||||
call(ProfileApiService.getAccount, username),
|
||||
call(ProfileApiService.getCourseCertificates, username),
|
||||
call(ProfileApiService.getCountryList),
|
||||
];
|
||||
|
||||
if (isAuthenticatedUserProfile) {
|
||||
calls.push(call(ProfileApiService.getPreferences, username));
|
||||
}
|
||||
|
||||
const result = yield all(calls);
|
||||
|
||||
if (isAuthenticatedUserProfile) {
|
||||
[account, courseCertificates, countriesCodesList, preferences] = result;
|
||||
} else {
|
||||
[account, courseCertificates, countriesCodesList] = result;
|
||||
}
|
||||
|
||||
if (isAuthenticatedUserProfile && result[0].accountPrivacy === 'all_users') {
|
||||
yield call(ProfileApiService.patchPreferences, action.payload.username, {
|
||||
account_privacy: 'custom',
|
||||
'visibility.name': 'all_users',
|
||||
'visibility.bio': 'all_users',
|
||||
'visibility.course_certificates': 'all_users',
|
||||
'visibility.country': 'all_users',
|
||||
'visibility.date_joined': 'all_users',
|
||||
'visibility.level_of_education': 'all_users',
|
||||
'visibility.language_proficiencies': 'all_users',
|
||||
'visibility.social_links': 'all_users',
|
||||
'visibility.time_zone': 'all_users',
|
||||
});
|
||||
}
|
||||
|
||||
yield put(fetchProfileSuccess(
|
||||
account,
|
||||
preferences,
|
||||
courseCertificates,
|
||||
isAuthenticatedUserProfile,
|
||||
countriesCodesList,
|
||||
));
|
||||
|
||||
yield put(fetchProfileReset());
|
||||
} catch (e) {
|
||||
if (e.response.status === 404) {
|
||||
history.push('/notfound');
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
yield put(saveProfilePhotoBegin());
|
||||
const photoResult = yield call(ProfileApiService.postProfilePhoto, username, formData);
|
||||
yield put(saveProfilePhotoSuccess(photoResult));
|
||||
yield put(saveProfilePhotoReset());
|
||||
} catch (e) {
|
||||
yield put(saveProfilePhotoReset());
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleDeleteProfilePhoto(action) {
|
||||
const { username } = action.payload;
|
||||
|
||||
try {
|
||||
yield put(deleteProfilePhotoBegin());
|
||||
const photoResult = yield call(ProfileApiService.deleteProfilePhoto, username);
|
||||
yield put(deleteProfilePhotoSuccess(photoResult));
|
||||
yield put(deleteProfilePhotoReset());
|
||||
} catch (e) {
|
||||
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);
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
import {
|
||||
takeEvery,
|
||||
put,
|
||||
call,
|
||||
delay,
|
||||
select,
|
||||
all,
|
||||
} from 'redux-saga/effects';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import * as profileActions from './actions';
|
||||
import { handleSaveProfileSelector, userAccountSelector } from './selectors';
|
||||
|
||||
jest.mock('./services', () => ({
|
||||
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', () => {
|
||||
const gen = profileSaga();
|
||||
|
||||
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)
|
||||
.toEqual(takeEvery(profileActions.DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto));
|
||||
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleFetchProfile', () => {
|
||||
it('should fetch certificates and preferences for the current user profile', () => {
|
||||
const userAccount = {
|
||||
username: 'gonzo',
|
||||
other: 'data',
|
||||
};
|
||||
getAuthenticatedUser.mockReturnValue(userAccount);
|
||||
const selectorData = {
|
||||
userAccount,
|
||||
};
|
||||
|
||||
const action = profileActions.fetchProfile('gonzo');
|
||||
const gen = handleFetchProfile(action);
|
||||
|
||||
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[3], result[1], true, [])));
|
||||
expect(gen.next().value).toEqual(put(profileActions.fetchProfileReset()));
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should fetch certificates and profile for some other user profile', () => {
|
||||
const userAccount = {
|
||||
username: 'gonzo',
|
||||
other: 'data',
|
||||
};
|
||||
const countriesCodesList = [{ code: 'AX' }, { code: 'AL' }];
|
||||
getAuthenticatedUser.mockReturnValue(userAccount);
|
||||
const selectorData = {
|
||||
userAccount,
|
||||
};
|
||||
|
||||
const action = profileActions.fetchProfile('booyah');
|
||||
const gen = handleFetchProfile(action);
|
||||
|
||||
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, countriesCodesList)));
|
||||
expect(gen.next().value).toEqual(put(profileActions.fetchProfileReset()));
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleSaveProfile', () => {
|
||||
const selectorData = {
|
||||
username: 'my username',
|
||||
drafts: {
|
||||
name: 'Full Name',
|
||||
},
|
||||
preferences: {},
|
||||
};
|
||||
|
||||
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 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(select(handleSaveProfileSelector));
|
||||
expect(gen.next(selectorData).value).toEqual(put(profileActions.saveProfileBegin()));
|
||||
const result = gen.throw(error);
|
||||
expect(result.value).toEqual(put(profileActions.saveProfileFailure({ uhoh: 'not good' })));
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,338 +0,0 @@
|
||||
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,
|
||||
(editableForm, certificates) => ({
|
||||
...editableForm,
|
||||
certificates,
|
||||
value: certificates,
|
||||
}),
|
||||
);
|
||||
|
||||
export const profileImageSelector = createSelector(
|
||||
profileAccountSelector,
|
||||
account => (account.profileImage != null
|
||||
? {
|
||||
src: account.profileImage.imageUrlFull,
|
||||
isDefault: !account.profileImage.hasImage,
|
||||
}
|
||||
: {}),
|
||||
);
|
||||
|
||||
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,
|
||||
formValuesSelector,
|
||||
profileImageSelector,
|
||||
saveStateSelector,
|
||||
savePhotoStateSelector,
|
||||
isLoadingProfileSelector,
|
||||
draftSocialLinksByPlatformSelector,
|
||||
accountErrorsSelector,
|
||||
isAuthenticatedUserProfileSelector,
|
||||
(
|
||||
account,
|
||||
formValues,
|
||||
profileImage,
|
||||
saveState,
|
||||
savePhotoState,
|
||||
isLoadingProfile,
|
||||
draftSocialLinksByPlatform,
|
||||
errors,
|
||||
isAuthenticatedUserProfile,
|
||||
) => ({
|
||||
username: account.username,
|
||||
profileImage,
|
||||
requiresParentalConsent: account.requiresParentalConsent,
|
||||
dateJoined: account.dateJoined,
|
||||
yearOfBirth: account.yearOfBirth,
|
||||
|
||||
bio: formValues.bio,
|
||||
visibilityBio: formValues.visibilityBio,
|
||||
|
||||
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,
|
||||
}),
|
||||
);
|
||||
@@ -1,168 +0,0 @@
|
||||
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) {
|
||||
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) {
|
||||
const processedError = Object.create(error);
|
||||
if (error.response && error.response.data && typeof error.response.data === 'object') {
|
||||
processedError.processedData = errorDataProcessor(error.response.data);
|
||||
throw processedError;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAccount(username) {
|
||||
const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`);
|
||||
|
||||
return processAccountData(data);
|
||||
}
|
||||
|
||||
export async function patchProfile(username, params) {
|
||||
const processedParams = snakeCaseObject(params);
|
||||
|
||||
const { data } = await getHttpClient()
|
||||
.patch(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`, processedParams, {
|
||||
headers: {
|
||||
'Content-Type': 'application/merge-patch+json',
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
processAndThrowError(error, processAccountData);
|
||||
});
|
||||
|
||||
return processAccountData(data);
|
||||
}
|
||||
|
||||
export async function postProfilePhoto(username, formData) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { data } = await getHttpClient().post(
|
||||
`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
},
|
||||
).catch((error) => {
|
||||
processAndThrowError(error, camelCaseObject);
|
||||
});
|
||||
|
||||
// TODO: Someday in the future the POST photo endpoint
|
||||
// will return the new values. At that time we should
|
||||
// use the commented line below instead of the separate
|
||||
// getAccount request that follows.
|
||||
// return camelCaseObject(data);
|
||||
const updatedData = await getAccount(username);
|
||||
return updatedData.profileImage;
|
||||
}
|
||||
|
||||
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`);
|
||||
|
||||
// TODO: Someday in the future the POST photo endpoint
|
||||
// will return the new values. At that time we should
|
||||
// use the commented line below instead of the separate
|
||||
// getAccount request that follows.
|
||||
// return camelCaseObject(data);
|
||||
const updatedData = await getAccount(username);
|
||||
return updatedData.profileImage;
|
||||
}
|
||||
|
||||
export async function getPreferences(username) {
|
||||
const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`);
|
||||
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
export async function patchPreferences(username, params) {
|
||||
let processedParams = snakeCaseObject(params);
|
||||
processedParams = convertKeyNames(processedParams, {
|
||||
visibility_bio: 'visibility.bio',
|
||||
visibility_course_certificates: 'visibility.course_certificates',
|
||||
visibility_country: 'visibility.country',
|
||||
visibility_date_joined: 'visibility.date_joined',
|
||||
visibility_level_of_education: 'visibility.level_of_education',
|
||||
visibility_language_proficiencies: 'visibility.language_proficiencies',
|
||||
visibility_name: 'visibility.name',
|
||||
visibility_social_links: 'visibility.social_links',
|
||||
visibility_time_zone: 'visibility.time_zone',
|
||||
});
|
||||
|
||||
await getHttpClient().patch(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`, processedParams, {
|
||||
headers: { 'Content-Type': 'application/merge-patch+json' },
|
||||
});
|
||||
|
||||
return params; // TODO: Once the server returns the updated preferences object, return that.
|
||||
}
|
||||
|
||||
function transformCertificateData(data) {
|
||||
const transformedData = [];
|
||||
data.forEach((cert) => {
|
||||
// download_url may be full url or absolute path.
|
||||
// note: using the URL() api breaks in ie 11
|
||||
const urlIsPath = typeof cert.download_url === 'string'
|
||||
&& cert.download_url.search(/http[s]?:\/\//) !== 0;
|
||||
|
||||
const downloadUrl = urlIsPath
|
||||
? `${getConfig().LMS_BASE_URL}${cert.download_url}`
|
||||
: cert.download_url;
|
||||
|
||||
transformedData.push({
|
||||
...camelCaseObject(cert),
|
||||
certificateType: cert.certificate_type,
|
||||
downloadUrl,
|
||||
});
|
||||
});
|
||||
return transformedData;
|
||||
}
|
||||
|
||||
export async function getCourseCertificates(username) {
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/certificates/v0/certificates/${username}/`;
|
||||
try {
|
||||
const { data } = await getHttpClient().get(url);
|
||||
return transformCertificateData(data);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
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);
|
||||
@@ -1,11 +0,0 @@
|
||||
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;
|
||||
@@ -1,163 +0,0 @@
|
||||
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);
|
||||
@@ -1,16 +0,0 @@
|
||||
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 user’s profile.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,171 +0,0 @@
|
||||
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);
|
||||
@@ -1,56 +0,0 @@
|
||||
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;
|
||||
@@ -1,192 +0,0 @@
|
||||
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);
|
||||
@@ -1,26 +0,0 @@
|
||||
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 user’s profile.',
|
||||
},
|
||||
'profile.name.tooltip': {
|
||||
id: 'profile.name.tooltip',
|
||||
defaultMessage: 'The name that is used for ID verification and that appears on your certificates',
|
||||
description: 'Tooltip for the full name field.',
|
||||
},
|
||||
'profile.name.redirect': {
|
||||
id: 'profile.name.redirect',
|
||||
defaultMessage: 'Edit full name from the Accounts page',
|
||||
description: 'Redirect message for editing the name from the Accounts page.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,166 +0,0 @@
|
||||
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);
|
||||
@@ -1,16 +0,0 @@
|
||||
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 doesn’t have a preferred language set.',
|
||||
},
|
||||
'profile.preferredlanguage.label': {
|
||||
id: 'profile.preferredlanguage.label',
|
||||
defaultMessage: 'Primary language spoken',
|
||||
description: 'The label for a user’s primary spoken language.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,170 +0,0 @@
|
||||
import React, { useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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';
|
||||
|
||||
const ProfileAvatar = ({
|
||||
src,
|
||||
isDefault,
|
||||
onSave,
|
||||
onDelete,
|
||||
savePhotoState,
|
||||
isEditable,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const fileInput = useRef(null);
|
||||
const form = useRef(null);
|
||||
|
||||
const onClickUpload = () => {
|
||||
fileInput.current.click();
|
||||
};
|
||||
|
||||
const onClickDelete = () => {
|
||||
onDelete();
|
||||
};
|
||||
|
||||
const onSubmit = (e) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
onSave(new FormData(form.current));
|
||||
form.current.reset();
|
||||
};
|
||||
|
||||
const onChangeInput = () => {
|
||||
onSubmit();
|
||||
};
|
||||
|
||||
const renderPending = () => (
|
||||
<div
|
||||
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 renderEditButton = () => {
|
||||
if (!isEditable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAvatar = () => (
|
||||
isDefault ? (
|
||||
<DefaultAvatar className="text-muted" role="img" aria-hidden focusable="false" viewBox="0 0 24 24" />
|
||||
) : (
|
||||
<img
|
||||
data-hj-suppress
|
||||
className="w-100 h-100 d-block rounded-circle overflow-hidden object-fit-cover"
|
||||
alt={intl.formatMessage(messages['profile.image.alt.attribute'])}
|
||||
src={src}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="profile-avatar-wrap position-relative">
|
||||
<div className="profile-avatar rounded-circle bg-light">
|
||||
{savePhotoState === 'pending' && renderPending()}
|
||||
{renderAvatar()}
|
||||
</div>
|
||||
{renderEditButton()}
|
||||
<form
|
||||
ref={form}
|
||||
onSubmit={onSubmit}
|
||||
encType="multipart/form-data"
|
||||
>
|
||||
<input
|
||||
className="d-none form-control-file"
|
||||
ref={fileInput}
|
||||
type="file"
|
||||
name="file"
|
||||
id="photo-file"
|
||||
onChange={onChangeInput}
|
||||
accept=".jpg, .jpeg, .png"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProfileAvatar.propTypes = {
|
||||
src: PropTypes.string,
|
||||
isDefault: PropTypes.bool,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
isEditable: PropTypes.bool,
|
||||
};
|
||||
|
||||
ProfileAvatar.defaultProps = {
|
||||
src: null,
|
||||
isDefault: true,
|
||||
savePhotoState: null,
|
||||
isEditable: false,
|
||||
};
|
||||
|
||||
export default ProfileAvatar;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'profile.image.alt.attribute': {
|
||||
id: 'profile.image.alt.attribute',
|
||||
defaultMessage: 'profile avatar',
|
||||
description: 'Alt attribute for a profile photo',
|
||||
},
|
||||
'profile.profileavatar.change-button': {
|
||||
id: 'profile.profileavatar.change-button',
|
||||
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;
|
||||
@@ -1,258 +0,0 @@
|
||||
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);
|
||||
@@ -1,11 +0,0 @@
|
||||
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;
|
||||
@@ -1,46 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
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;
|
||||
@@ -1,69 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
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;
|
||||
@@ -1,59 +0,0 @@
|
||||
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;
|
||||
@@ -1,76 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
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;
|
||||
@@ -1,5 +0,0 @@
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { default as ProfilePage } from './ProfilePage';
|
||||
export { default as NotFoundPage } from './NotFoundPage';
|
||||
export { default as messages } from './ProfilePage.messages';
|
||||
@@ -1,265 +0,0 @@
|
||||
.word-break-all {
|
||||
word-break: break-all !important;
|
||||
}
|
||||
|
||||
// TODO: Update edx-bootstrap theme to incorporate these edits.
|
||||
.btn, a.btn {
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
.btn-link {
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-page-bg-banner {
|
||||
height: 298px;
|
||||
width: 100%;
|
||||
background-image: url('./assets/dot-pattern-light.png');
|
||||
background-repeat: repeat-x;
|
||||
background-size: auto 85%;
|
||||
}
|
||||
|
||||
.icon-visibility-off {
|
||||
height: 1rem;
|
||||
color: var(--pgn-color-gray-500);
|
||||
}
|
||||
|
||||
.profile-page {
|
||||
.edit-section-header {
|
||||
font-size: var(--pgn-typography-font-size-h4-base);
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
margin: 0;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
|
||||
label.edit-section-header {
|
||||
margin-bottom: calc(var(--pgn-spacing-spacer-base) * .5);
|
||||
}
|
||||
|
||||
.profile-avatar-wrap {
|
||||
@media (--pgn-size-breakpoint-min-width-md) {
|
||||
max-width: 12rem;
|
||||
margin-right: 0;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-avatar-button {
|
||||
position: absolute;
|
||||
left: 76px;
|
||||
top: 76px;
|
||||
}
|
||||
|
||||
.profile-avatar-menu-container {
|
||||
background: rgba(0,0,0,.65);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
|
||||
@media (--pgn-size-breakpoint-min-width-md) {
|
||||
background: linear-gradient(to top, rgba(0,0,0,.65) 4rem, rgba(0,0,0,0) 4rem);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.btn {
|
||||
text-decoration: none;
|
||||
@media (--pgn-size-breakpoint-min-width-md) {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
@media (--pgn-size-breakpoint-min-width-md) {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
color:var(--pgn-color-white);;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 7.5rem;
|
||||
height: 7.5rem;
|
||||
position: relative;
|
||||
|
||||
@media (--pgn-size-breakpoint-min-width-md) {
|
||||
width: 7.5rem;
|
||||
height: 7.5rem;
|
||||
}
|
||||
|
||||
.profile-avatar-edit-button {
|
||||
border: none;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: .1rem;
|
||||
font-weight: 600;
|
||||
background: rgba(0,0,0,.5);
|
||||
border-radius:0;
|
||||
transition: opacity 200ms ease;
|
||||
|
||||
@media (--pgn-size-breakpoint-min-width-md) {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
&:focus, &:hover, &:active, &.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.certificate {
|
||||
background-color: #F3F1ED;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
border: 1px #E7E4DB solid;
|
||||
|
||||
.certificate-type-illustration {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
bottom: 0;
|
||||
width: 15.15rem;
|
||||
opacity: .06;
|
||||
background-size: 90%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding-left: 0.125rem;
|
||||
}
|
||||
|
||||
.max-width-32em {
|
||||
max-width: 32em;
|
||||
}
|
||||
|
||||
.height-50vh {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
// Todo: Move the following to edx-paragon
|
||||
|
||||
.btn-rounded {
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.min-width-179px {
|
||||
min-width: 179px;
|
||||
}
|
||||
|
||||
.max-width-304px{
|
||||
max-width: 304px;
|
||||
}
|
||||
|
||||
.width-314px {
|
||||
width: 314px;
|
||||
}
|
||||
|
||||
.w-90{
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.width-24px{
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.height-42px {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.rounded-75 {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.pt-40px{
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.pl-40px {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.py-10px{
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.py-36px {
|
||||
padding-top: 36px;
|
||||
padding-bottom: 36px;
|
||||
}
|
||||
|
||||
.px-120px {
|
||||
padding-left: 120px;
|
||||
padding-right: 120px;
|
||||
}
|
||||
|
||||
.px-40px {
|
||||
padding-left: 40px;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.g-15rem {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.g-5rem {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.g-1rem {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.g-3rem {
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.color-black {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.bg-color-grey-FBFAF9 {
|
||||
background-color: #FBFAF9;
|
||||
}
|
||||
|
||||
.background-black-65 {
|
||||
background-color: rgba(0,0,0,.65)
|
||||
}
|
||||
|
||||
.object-fit-cover {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.lh-36px {
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.overflowWrap-breakWord {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import snakeCase from 'lodash.snakecase';
|
||||
|
||||
export function modifyObjectKeys(object, modify) {
|
||||
if (
|
||||
object === undefined
|
||||
|| object === null
|
||||
|| (typeof object !== 'object' && !Array.isArray(object))
|
||||
) {
|
||||
return object;
|
||||
}
|
||||
|
||||
if (Array.isArray(object)) {
|
||||
return object.map(value => modifyObjectKeys(value, modify));
|
||||
}
|
||||
|
||||
const result = {};
|
||||
Object.entries(object).forEach(([key, value]) => {
|
||||
result[modify(key)] = modifyObjectKeys(value, modify);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function camelCaseObject(object) {
|
||||
return modifyObjectKeys(object, camelCase);
|
||||
}
|
||||
|
||||
export function snakeCaseObject(object) {
|
||||
return modifyObjectKeys(object, snakeCase);
|
||||
}
|
||||
|
||||
export function convertKeyNames(object, nameMap) {
|
||||
const transformer = key => (nameMap[key] === undefined ? key : nameMap[key]);
|
||||
|
||||
return modifyObjectKeys(object, transformer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to save time when writing out action types for asynchronous methods. Also helps
|
||||
* ensure that actions are namespaced.
|
||||
*
|
||||
* TODO: Put somewhere common to it can be used by other MFEs.
|
||||
*/
|
||||
export class AsyncActionType {
|
||||
constructor(topic, name) {
|
||||
this.topic = topic;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
get BASE() {
|
||||
return `${this.topic}__${this.name}`;
|
||||
}
|
||||
|
||||
get BEGIN() {
|
||||
return `${this.topic}__${this.name}__BEGIN`;
|
||||
}
|
||||
|
||||
get SUCCESS() {
|
||||
return `${this.topic}__${this.name}__SUCCESS`;
|
||||
}
|
||||
|
||||
get FAILURE() {
|
||||
return `${this.topic}__${this.name}__FAILURE`;
|
||||
}
|
||||
|
||||
get RESET() {
|
||||
return `${this.topic}__${this.name}__RESET`;
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import {
|
||||
AsyncActionType,
|
||||
modifyObjectKeys,
|
||||
camelCaseObject,
|
||||
snakeCaseObject,
|
||||
convertKeyNames,
|
||||
} from './utils';
|
||||
|
||||
describe('modifyObjectKeys', () => {
|
||||
it('should use the provided modify function to change all keys in and object and its children', () => {
|
||||
function meowKeys(key) {
|
||||
return `${key}Meow`;
|
||||
}
|
||||
|
||||
const result = modifyObjectKeys(
|
||||
{
|
||||
one: undefined,
|
||||
two: null,
|
||||
three: '',
|
||||
four: 0,
|
||||
five: NaN,
|
||||
six: [1, 2, { seven: 'woof' }],
|
||||
eight: { nine: { ten: 'bark' }, eleven: true },
|
||||
},
|
||||
meowKeys,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
oneMeow: undefined,
|
||||
twoMeow: null,
|
||||
threeMeow: '',
|
||||
fourMeow: 0,
|
||||
fiveMeow: NaN,
|
||||
sixMeow: [1, 2, { sevenMeow: 'woof' }],
|
||||
eightMeow: { nineMeow: { tenMeow: 'bark' }, elevenMeow: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('camelCaseObject', () => {
|
||||
it('should make everything camelCase', () => {
|
||||
const result = camelCaseObject({
|
||||
what_now: 'brown cow',
|
||||
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
|
||||
'dot.dot.dot': 123,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
whatNow: 'brown cow',
|
||||
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
|
||||
dotDotDot: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('snakeCaseObject', () => {
|
||||
it('should make everything snake_case', () => {
|
||||
const result = snakeCaseObject({
|
||||
whatNow: 'brown cow',
|
||||
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
|
||||
'dot.dot.dot': 123,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
what_now: 'brown cow',
|
||||
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
|
||||
dot_dot_dot: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertKeyNames', () => {
|
||||
it('should replace the specified keynames', () => {
|
||||
const result = convertKeyNames(
|
||||
{
|
||||
one: { two: { three: 'four' } },
|
||||
five: 'six',
|
||||
},
|
||||
{
|
||||
two: 'blue',
|
||||
five: 'alive',
|
||||
seven: 'heaven',
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
one: { blue: { three: 'four' } },
|
||||
alive: 'six',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AsyncActionType', () => {
|
||||
it('should return well formatted action strings', () => {
|
||||
const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE');
|
||||
|
||||
expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE');
|
||||
expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN');
|
||||
expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
|
||||
expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
|
||||
expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
|
||||
});
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
const AgeMessage = ({ accountSettingsUrl }) => (
|
||||
<Alert
|
||||
variant="info"
|
||||
dismissible={false}
|
||||
show
|
||||
>
|
||||
<Alert.Heading id="profile.age.headline">
|
||||
<FormattedMessage
|
||||
id="profile.age.cannotShare"
|
||||
defaultMessage="Your profile cannot be shared."
|
||||
description="Error message indicating that the user's profile cannot be shared"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<FormattedMessage
|
||||
id="profile.age.details"
|
||||
defaultMessage="To share your profile with other {siteName} learners, you must confirm that you are over the age of 13."
|
||||
description="Error message"
|
||||
tagName="p"
|
||||
values={{
|
||||
siteName: getConfig().SITE_NAME,
|
||||
}}
|
||||
/>
|
||||
<Alert.Link href={accountSettingsUrl}>
|
||||
<FormattedMessage
|
||||
id="profile.age.set.date"
|
||||
defaultMessage="Set your date of birth"
|
||||
description="Label on a link to set birthday"
|
||||
/>
|
||||
</Alert.Link>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
AgeMessage.propTypes = {
|
||||
accountSettingsUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default AgeMessage;
|
||||
@@ -1,5 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Banner = () => <div className="profile-page-bg-banner bg-primary d-md-block p-relative" />;
|
||||
|
||||
export default Banner;
|
||||
@@ -1,23 +1,21 @@
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const DateJoined = ({ date }) => {
|
||||
if (date == null) {
|
||||
return null;
|
||||
}
|
||||
if (!date) { return null; }
|
||||
|
||||
return (
|
||||
<p className="mb-0">
|
||||
<span className="small mb-0 text-gray-800">
|
||||
<FormattedMessage
|
||||
id="profile.datejoined.member.since"
|
||||
defaultMessage="Member since {year}"
|
||||
description="A label for how long the user has been a member"
|
||||
values={{
|
||||
year: <FormattedDate value={new Date(date)} year="numeric" />,
|
||||
year: <span className="font-weight-bold"> <FormattedDate value={new Date(date)} year="numeric" /> </span>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,4 +26,4 @@ DateJoined.defaultProps = {
|
||||
date: null,
|
||||
};
|
||||
|
||||
export default DateJoined;
|
||||
export default memo(DateJoined);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const NotFoundPage = () => (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
<p className="my-0 py-5 text-muted" style={{ maxWidth: '32em' }}>
|
||||
<p className="my-0 py-5 text-muted max-width-32em">
|
||||
<FormattedMessage
|
||||
id="profile.notfound.message"
|
||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||
|
||||
@@ -1,37 +1,18 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class PageLoading extends Component {
|
||||
renderSrMessage() {
|
||||
if (!this.props.srMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="sr-only">
|
||||
{this.props.srMessage}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center flex-column"
|
||||
style={{
|
||||
height: '50vh',
|
||||
}}
|
||||
>
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
{this.renderSrMessage()}
|
||||
</div>
|
||||
</div>
|
||||
const PageLoading = ({ srMessage }) => (
|
||||
<div>
|
||||
<div className="d-flex justify-content-center align-items-center flex-column height-50vh">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
{srMessage && <span className="sr-only">{srMessage}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
PageLoading.propTypes = {
|
||||
srMessage: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PageLoading;
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import React from 'react';
|
||||
import React, {
|
||||
useEffect, useState, useContext, useCallback,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { ensureConfig } from '@edx/frontend-platform';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert, Hyperlink, OverlayTrigger, Tooltip,
|
||||
} from '@openedx/paragon';
|
||||
import { InfoOutline } from '@openedx/paragon/icons';
|
||||
import classNames from 'classnames';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
fetchProfile,
|
||||
saveProfile,
|
||||
@@ -19,7 +25,6 @@ import {
|
||||
updateDraft,
|
||||
} from './data/actions';
|
||||
|
||||
// Components
|
||||
import ProfileAvatar from './forms/ProfileAvatar';
|
||||
import Name from './forms/Name';
|
||||
import Country from './forms/Country';
|
||||
@@ -27,120 +32,122 @@ import PreferredLanguage from './forms/PreferredLanguage';
|
||||
import Education from './forms/Education';
|
||||
import SocialLinks from './forms/SocialLinks';
|
||||
import Bio from './forms/Bio';
|
||||
import Certificates from './forms/Certificates';
|
||||
import AgeMessage from './AgeMessage';
|
||||
import DateJoined from './DateJoined';
|
||||
import UsernameDescription from './UsernameDescription';
|
||||
import UserCertificateSummary from './UserCertificateSummary';
|
||||
import PageLoading from './PageLoading';
|
||||
import Banner from './Banner';
|
||||
import LearningGoal from './forms/LearningGoal';
|
||||
import Certificates from './Certificates';
|
||||
|
||||
// Selectors
|
||||
import { profilePageSelector } from './data/selectors';
|
||||
|
||||
// i18n
|
||||
import messages from './ProfilePage.messages';
|
||||
|
||||
import withParams from '../utils/hoc';
|
||||
import { useIsOnMobileScreen, useIsOnTabletScreen } from './data/hooks';
|
||||
|
||||
ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
|
||||
ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL', 'ACCOUNT_SETTINGS_URL'], 'ProfilePage');
|
||||
|
||||
class ProfilePage extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
const ProfilePage = ({ params }) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const context = useContext(AppContext);
|
||||
const {
|
||||
dateJoined,
|
||||
courseCertificates,
|
||||
name,
|
||||
visibilityName,
|
||||
profileImage,
|
||||
savePhotoState,
|
||||
isLoadingProfile,
|
||||
photoUploadError,
|
||||
country,
|
||||
visibilityCountry,
|
||||
levelOfEducation,
|
||||
visibilityLevelOfEducation,
|
||||
socialLinks,
|
||||
draftSocialLinksByPlatform,
|
||||
visibilitySocialLinks,
|
||||
languageProficiencies,
|
||||
visibilityLanguageProficiencies,
|
||||
bio,
|
||||
visibilityBio,
|
||||
saveState,
|
||||
username,
|
||||
} = useSelector(profilePageSelector);
|
||||
|
||||
const credentialsBaseUrl = context.config.CREDENTIALS_BASE_URL;
|
||||
this.state = {
|
||||
viewMyRecordsUrl: credentialsBaseUrl ? `${credentialsBaseUrl}/records` : null,
|
||||
accountSettingsUrl: context.config.ACCOUNT_SETTINGS_URL,
|
||||
};
|
||||
const navigate = useNavigate();
|
||||
const [viewMyRecordsUrl, setViewMyRecordsUrl] = useState(null);
|
||||
const isMobileView = useIsOnMobileScreen();
|
||||
const isTabletView = useIsOnTabletScreen();
|
||||
|
||||
this.handleSaveProfilePhoto = this.handleSaveProfilePhoto.bind(this);
|
||||
this.handleDeleteProfilePhoto = this.handleDeleteProfilePhoto.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.handleOpen = this.handleOpen.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
useEffect(() => {
|
||||
const { CREDENTIALS_BASE_URL } = context.config;
|
||||
if (CREDENTIALS_BASE_URL) {
|
||||
setViewMyRecordsUrl(`${CREDENTIALS_BASE_URL}/records`);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchProfile(this.props.params.username);
|
||||
dispatch(fetchProfile(params.username));
|
||||
sendTrackingLogEvent('edx.profile.viewed', {
|
||||
username: this.props.params.username,
|
||||
username: params.username,
|
||||
});
|
||||
}
|
||||
}, [dispatch, params.username, context.config]);
|
||||
|
||||
handleSaveProfilePhoto(formData) {
|
||||
this.props.saveProfilePhoto(this.context.authenticatedUser.username, formData);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!username && saveState === 'error' && navigate) {
|
||||
navigate('/notfound');
|
||||
}
|
||||
}, [username, saveState, navigate]);
|
||||
|
||||
handleDeleteProfilePhoto() {
|
||||
this.props.deleteProfilePhoto(this.context.authenticatedUser.username);
|
||||
}
|
||||
const authenticatedUserName = context.authenticatedUser.username;
|
||||
|
||||
handleClose(formId) {
|
||||
this.props.closeForm(formId);
|
||||
}
|
||||
const handleSaveProfilePhoto = useCallback((formData) => {
|
||||
dispatch(saveProfilePhoto(authenticatedUserName, formData));
|
||||
}, [dispatch, authenticatedUserName]);
|
||||
|
||||
handleOpen(formId) {
|
||||
this.props.openForm(formId);
|
||||
}
|
||||
const handleDeleteProfilePhoto = useCallback(() => {
|
||||
dispatch(deleteProfilePhoto(authenticatedUserName));
|
||||
}, [dispatch, authenticatedUserName]);
|
||||
|
||||
handleSubmit(formId) {
|
||||
this.props.saveProfile(formId, this.context.authenticatedUser.username);
|
||||
}
|
||||
const handleClose = useCallback((formId) => {
|
||||
dispatch(closeForm(formId));
|
||||
}, [dispatch]);
|
||||
|
||||
handleChange(name, value) {
|
||||
this.props.updateDraft(name, value);
|
||||
}
|
||||
const handleOpen = useCallback((formId) => {
|
||||
dispatch(openForm(formId));
|
||||
}, [dispatch]);
|
||||
|
||||
isYOBDisabled() {
|
||||
const { yearOfBirth } = this.props;
|
||||
const currentYear = new Date().getFullYear();
|
||||
const isAgeOrNotCompliant = !yearOfBirth || ((currentYear - yearOfBirth) < 13);
|
||||
const handleSubmit = useCallback((formId) => {
|
||||
dispatch(saveProfile(formId, authenticatedUserName));
|
||||
}, [dispatch, authenticatedUserName]);
|
||||
|
||||
return isAgeOrNotCompliant && getConfig().COLLECT_YEAR_OF_BIRTH !== 'true';
|
||||
}
|
||||
const handleChange = useCallback((fieldName, value) => {
|
||||
dispatch(updateDraft(fieldName, value));
|
||||
}, [dispatch]);
|
||||
|
||||
isAuthenticatedUserProfile() {
|
||||
return this.props.params.username === this.context.authenticatedUser.username;
|
||||
}
|
||||
const isAuthenticatedUserProfile = () => params.username === authenticatedUserName;
|
||||
|
||||
// Inserted into the DOM in two places (for responsive layout)
|
||||
renderViewMyRecordsButton() {
|
||||
if (!(this.state.viewMyRecordsUrl && this.isAuthenticatedUserProfile())) {
|
||||
const isBlockVisible = (blockInfo) => isAuthenticatedUserProfile()
|
||||
|| (!isAuthenticatedUserProfile() && Boolean(blockInfo));
|
||||
|
||||
const renderViewMyRecordsButton = () => {
|
||||
if (!(viewMyRecordsUrl && isAuthenticatedUserProfile())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Hyperlink className="btn btn-primary" destination={this.state.viewMyRecordsUrl} target="_blank">
|
||||
{this.props.intl.formatMessage(messages['profile.viewMyRecords'])}
|
||||
<Hyperlink
|
||||
className={classNames(
|
||||
'btn btn-brand bg-brand-500 btn-rounded font-weight-normal px-4 py-10px text-nowrap',
|
||||
{ 'w-100': isMobileView },
|
||||
)}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
destination={viewMyRecordsUrl}
|
||||
>
|
||||
{intl.formatMessage(messages['profile.viewMyRecords'])}
|
||||
</Hyperlink>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Inserted into the DOM in two places (for responsive layout)
|
||||
renderHeadingLockup() {
|
||||
const { dateJoined } = this.props;
|
||||
|
||||
return (
|
||||
<span data-hj-suppress>
|
||||
<h1 className="h2 mb-0 font-weight-bold text-truncate">{this.props.params.username}</h1>
|
||||
<DateJoined date={dateJoined} />
|
||||
{this.isYOBDisabled() && <UsernameDescription />}
|
||||
<hr className="d-none d-md-block" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
renderPhotoUploadErrorMessage() {
|
||||
const { photoUploadError } = this.props;
|
||||
|
||||
if (photoUploadError === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
const renderPhotoUploadErrorMessage = () => (
|
||||
photoUploadError && (
|
||||
<div className="row">
|
||||
<div className="col-md-4 col-lg-3">
|
||||
<Alert variant="danger" dismissible={false} show>
|
||||
@@ -148,227 +155,267 @@ class ProfilePage extends React.Component {
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
renderAgeMessage() {
|
||||
const { requiresParentalConsent } = this.props;
|
||||
const shouldShowAgeMessage = requiresParentalConsent && this.isAuthenticatedUserProfile();
|
||||
const commonFormProps = {
|
||||
openHandler: handleOpen,
|
||||
closeHandler: handleClose,
|
||||
submitHandler: handleSubmit,
|
||||
changeHandler: handleChange,
|
||||
};
|
||||
|
||||
if (!shouldShowAgeMessage) {
|
||||
return null;
|
||||
}
|
||||
return <AgeMessage accountSettingsUrl={this.state.accountSettingsUrl} />;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const {
|
||||
profileImage,
|
||||
name,
|
||||
visibilityName,
|
||||
country,
|
||||
visibilityCountry,
|
||||
levelOfEducation,
|
||||
visibilityLevelOfEducation,
|
||||
socialLinks,
|
||||
draftSocialLinksByPlatform,
|
||||
visibilitySocialLinks,
|
||||
learningGoal,
|
||||
visibilityLearningGoal,
|
||||
languageProficiencies,
|
||||
visibilityLanguageProficiencies,
|
||||
courseCertificates,
|
||||
visibilityCourseCertificates,
|
||||
bio,
|
||||
visibilityBio,
|
||||
requiresParentalConsent,
|
||||
isLoadingProfile,
|
||||
username,
|
||||
saveState,
|
||||
navigate,
|
||||
} = this.props;
|
||||
|
||||
if (isLoadingProfile) {
|
||||
return <PageLoading srMessage={this.props.intl.formatMessage(messages['profile.loading'])} />;
|
||||
}
|
||||
|
||||
if (!username && saveState === 'error' && navigate) {
|
||||
navigate('/notfound');
|
||||
}
|
||||
|
||||
const commonFormProps = {
|
||||
openHandler: this.handleOpen,
|
||||
closeHandler: this.handleClose,
|
||||
submitHandler: this.handleSubmit,
|
||||
changeHandler: this.handleChange,
|
||||
};
|
||||
|
||||
const isBlockVisible = (blockInfo) => this.isAuthenticatedUserProfile()
|
||||
|| (!this.isAuthenticatedUserProfile() && Boolean(blockInfo));
|
||||
|
||||
const isLanguageBlockVisible = isBlockVisible(languageProficiencies.length);
|
||||
const isEducationBlockVisible = isBlockVisible(levelOfEducation);
|
||||
const isSocialLinksBLockVisible = isBlockVisible(socialLinks.some((link) => link.socialLink !== null));
|
||||
const isBioBlockVisible = isBlockVisible(bio);
|
||||
const isCertificatesBlockVisible = isBlockVisible(courseCertificates.length);
|
||||
const isNameBlockVisible = isBlockVisible(name);
|
||||
const isLocationBlockVisible = isBlockVisible(country);
|
||||
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="row align-items-center pt-4 mb-4 pt-md-0 mb-md-0">
|
||||
<div className="col-auto col-md-4 col-lg-3">
|
||||
<div className="d-flex align-items-center d-md-block">
|
||||
<ProfileAvatar
|
||||
className="mb-md-3"
|
||||
src={profileImage.src}
|
||||
isDefault={profileImage.isDefault}
|
||||
onSave={this.handleSaveProfilePhoto}
|
||||
onDelete={this.handleDeleteProfilePhoto}
|
||||
savePhotoState={this.props.savePhotoState}
|
||||
isEditable={this.isAuthenticatedUserProfile() && !requiresParentalConsent}
|
||||
/>
|
||||
return (
|
||||
<div className="profile-page">
|
||||
{isLoadingProfile ? (
|
||||
<PageLoading srMessage={intl.formatMessage(messages['profile.loading'])} />
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'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={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',
|
||||
isMobileView || isTabletView ? 'flex-column' : 'flex-row',
|
||||
])}
|
||||
>
|
||||
<ProfileAvatar
|
||||
className="col p-0"
|
||||
src={profileImage.src}
|
||||
isDefault={profileImage.isDefault}
|
||||
onSave={handleSaveProfilePhoto}
|
||||
onDelete={handleDeleteProfilePhoto}
|
||||
savePhotoState={savePhotoState}
|
||||
isEditable={isAuthenticatedUserProfile()}
|
||||
/>
|
||||
<div
|
||||
className={classNames([
|
||||
'col h-100 w-100 m-0 p-0',
|
||||
isMobileView || isTabletView
|
||||
? 'd-flex flex-column justify-content-center align-items-center'
|
||||
: 'justify-content-start align-items-start',
|
||||
])}
|
||||
>
|
||||
<p className="row m-0 font-weight-bold text-truncate text-primary-500 h3">
|
||||
{params.username}
|
||||
</p>
|
||||
{isBlockVisible(name) && (
|
||||
<p className="row pt-2 text-gray-800 font-weight-normal m-0 p">
|
||||
{name}
|
||||
</p>
|
||||
)}
|
||||
<div className={classNames(
|
||||
'row pt-2 m-0',
|
||||
isMobileView
|
||||
? 'd-flex justify-content-center align-items-center flex-column'
|
||||
: 'g-1rem',
|
||||
)}
|
||||
>
|
||||
<DateJoined date={dateJoined} />
|
||||
<UserCertificateSummary count={courseCertificates?.length || 0} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames([
|
||||
'p-0 ',
|
||||
isMobileView || isTabletView ? 'col d-flex justify-content-center' : 'col-auto',
|
||||
])}
|
||||
>
|
||||
{renderViewMyRecordsButton()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
{renderPhotoUploadErrorMessage()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="d-md-none">
|
||||
{this.renderHeadingLockup()}
|
||||
</div>
|
||||
<div className="d-none d-md-block float-right">
|
||||
{this.renderViewMyRecordsButton()}
|
||||
<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>
|
||||
{this.renderPhotoUploadErrorMessage()}
|
||||
<div className="row">
|
||||
<div className="col-md-4 col-lg-4">
|
||||
<div className="d-none d-md-block mb-4">
|
||||
{this.renderHeadingLockup()}
|
||||
</div>
|
||||
<div className="d-md-none mb-4">
|
||||
{this.renderViewMyRecordsButton()}
|
||||
</div>
|
||||
{isNameBlockVisible && (
|
||||
<Name
|
||||
name={name}
|
||||
visibilityName={visibilityName}
|
||||
formId="name"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{isLocationBlockVisible && (
|
||||
<Country
|
||||
country={country}
|
||||
visibilityCountry={visibilityCountry}
|
||||
formId="country"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{isLanguageBlockVisible && (
|
||||
<PreferredLanguage
|
||||
languageProficiencies={languageProficiencies}
|
||||
visibilityLanguageProficiencies={visibilityLanguageProficiencies}
|
||||
formId="languageProficiencies"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{isEducationBlockVisible && (
|
||||
<Education
|
||||
levelOfEducation={levelOfEducation}
|
||||
visibilityLevelOfEducation={visibilityLevelOfEducation}
|
||||
formId="levelOfEducation"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{isSocialLinksBLockVisible && (
|
||||
<SocialLinks
|
||||
socialLinks={socialLinks}
|
||||
draftSocialLinksByPlatform={draftSocialLinksByPlatform}
|
||||
visibilitySocialLinks={visibilitySocialLinks}
|
||||
formId="socialLinks"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<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>
|
||||
<div className="pt-md-3 col-md-8 col-lg-7 offset-lg-1">
|
||||
{!this.isYOBDisabled() && this.renderAgeMessage()}
|
||||
{isBioBlockVisible && (
|
||||
<Bio
|
||||
bio={bio}
|
||||
visibilityBio={visibilityBio}
|
||||
formId="bio"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{getConfig().ENABLE_SKILLS_BUILDER_PROFILE && (
|
||||
<LearningGoal
|
||||
learningGoal={learningGoal}
|
||||
visibilityLearningGoal={visibilityLearningGoal}
|
||||
formId="learningGoal"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{isCertificatesBlockVisible && (
|
||||
<Certificates
|
||||
visibilityCourseCertificates={visibilityCourseCertificates}
|
||||
formId="certificates"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="profile-page">
|
||||
<Banner />
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ProfilePage.contextType = AppContext;
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProfilePage.propTypes = {
|
||||
// Account data
|
||||
params: PropTypes.shape({
|
||||
username: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
requiresParentalConsent: PropTypes.bool,
|
||||
dateJoined: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
|
||||
// Bio form data
|
||||
bio: PropTypes.string,
|
||||
yearOfBirth: PropTypes.number,
|
||||
visibilityBio: PropTypes.string.isRequired,
|
||||
|
||||
// Certificates form data
|
||||
visibilityBio: PropTypes.string,
|
||||
courseCertificates: PropTypes.arrayOf(PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
})),
|
||||
visibilityCourseCertificates: PropTypes.string.isRequired,
|
||||
|
||||
// Country form data
|
||||
country: PropTypes.string,
|
||||
visibilityCountry: PropTypes.string.isRequired,
|
||||
|
||||
// Education form data
|
||||
visibilityCountry: PropTypes.string,
|
||||
levelOfEducation: PropTypes.string,
|
||||
visibilityLevelOfEducation: PropTypes.string.isRequired,
|
||||
|
||||
// Language proficiency form data
|
||||
visibilityLevelOfEducation: PropTypes.string,
|
||||
languageProficiencies: PropTypes.arrayOf(PropTypes.shape({
|
||||
code: PropTypes.string.isRequired,
|
||||
})),
|
||||
visibilityLanguageProficiencies: PropTypes.string.isRequired,
|
||||
|
||||
// Name form data
|
||||
visibilityLanguageProficiencies: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
visibilityName: PropTypes.string.isRequired,
|
||||
|
||||
// Social links form data
|
||||
visibilityName: PropTypes.string,
|
||||
socialLinks: PropTypes.arrayOf(PropTypes.shape({
|
||||
platform: PropTypes.string,
|
||||
socialLink: PropTypes.string,
|
||||
@@ -377,41 +424,15 @@ ProfilePage.propTypes = {
|
||||
platform: PropTypes.string,
|
||||
socialLink: PropTypes.string,
|
||||
})),
|
||||
visibilitySocialLinks: PropTypes.string.isRequired,
|
||||
|
||||
// Learning Goal form data
|
||||
learningGoal: PropTypes.string,
|
||||
visibilityLearningGoal: PropTypes.string.isRequired,
|
||||
|
||||
// Other data we need
|
||||
visibilitySocialLinks: PropTypes.string,
|
||||
profileImage: PropTypes.shape({
|
||||
src: PropTypes.string,
|
||||
isDefault: PropTypes.bool,
|
||||
}),
|
||||
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
isLoadingProfile: PropTypes.bool.isRequired,
|
||||
|
||||
// Page state helpers
|
||||
isLoadingProfile: PropTypes.bool,
|
||||
photoUploadError: PropTypes.objectOf(PropTypes.string),
|
||||
|
||||
// Actions
|
||||
fetchProfile: PropTypes.func.isRequired,
|
||||
saveProfile: PropTypes.func.isRequired,
|
||||
saveProfilePhoto: PropTypes.func.isRequired,
|
||||
deleteProfilePhoto: PropTypes.func.isRequired,
|
||||
openForm: PropTypes.func.isRequired,
|
||||
closeForm: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
navigate: PropTypes.func.isRequired,
|
||||
|
||||
// Router
|
||||
params: PropTypes.shape({
|
||||
username: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
// i18n
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
ProfilePage.defaultProps = {
|
||||
@@ -421,28 +442,22 @@ ProfilePage.defaultProps = {
|
||||
photoUploadError: {},
|
||||
profileImage: {},
|
||||
name: null,
|
||||
yearOfBirth: null,
|
||||
levelOfEducation: null,
|
||||
country: null,
|
||||
socialLinks: [],
|
||||
draftSocialLinksByPlatform: {},
|
||||
bio: null,
|
||||
learningGoal: null,
|
||||
languageProficiencies: [],
|
||||
courseCertificates: null,
|
||||
courseCertificates: [],
|
||||
requiresParentalConsent: null,
|
||||
dateJoined: null,
|
||||
visibilityName: null,
|
||||
visibilityCountry: null,
|
||||
visibilityLevelOfEducation: null,
|
||||
visibilitySocialLinks: null,
|
||||
visibilityLanguageProficiencies: null,
|
||||
visibilityBio: null,
|
||||
isLoadingProfile: false,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
profilePageSelector,
|
||||
{
|
||||
fetchProfile,
|
||||
saveProfilePhoto,
|
||||
deleteProfilePhoto,
|
||||
saveProfile,
|
||||
openForm,
|
||||
closeForm,
|
||||
updateDraft,
|
||||
},
|
||||
)(injectIntl(withParams(ProfilePage)));
|
||||
export default withParams(ProfilePage);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable global-require */
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import * as analytics from '@edx/frontend-platform/analytics';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
@@ -9,31 +8,38 @@ import PropTypes from 'prop-types';
|
||||
import { Provider } from 'react-redux';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { BrowserRouter, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
MemoryRouter,
|
||||
Routes,
|
||||
Route,
|
||||
useNavigate,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import messages from '../i18n';
|
||||
import ProfilePage from './ProfilePage';
|
||||
import loadingApp from './__mocks__/loadingApp.mockStore';
|
||||
import viewOwnProfile from './__mocks__/viewOwnProfile.mockStore';
|
||||
import viewOtherProfile from './__mocks__/viewOtherProfile.mockStore';
|
||||
import invalidUser from './__mocks__/invalidUser.mockStore';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
const storeMocks = {
|
||||
loadingApp: require('./__mocks__/loadingApp.mockStore'),
|
||||
invalidUser: require('./__mocks__/invalidUser.mockStore'),
|
||||
viewOwnProfile: require('./__mocks__/viewOwnProfile.mockStore'),
|
||||
viewOtherProfile: require('./__mocks__/viewOtherProfile.mockStore'),
|
||||
savingEditedBio: require('./__mocks__/savingEditedBio.mockStore'),
|
||||
loadingApp,
|
||||
viewOwnProfile,
|
||||
viewOtherProfile,
|
||||
invalidUser,
|
||||
};
|
||||
|
||||
const requiredProfilePageProps = {
|
||||
fetchUserAccount: () => {},
|
||||
fetchProfile: () => {},
|
||||
saveProfile: () => {},
|
||||
saveProfilePhoto: () => {},
|
||||
deleteProfilePhoto: () => {},
|
||||
openField: () => {},
|
||||
closeField: () => {},
|
||||
params: { username: 'staff' },
|
||||
};
|
||||
|
||||
// Mock language cookie
|
||||
Object.defineProperty(global.document, 'cookie', {
|
||||
writable: true,
|
||||
value: `${getConfig().LANGUAGE_PREFERENCE_COOKIE_NAME}=en`,
|
||||
@@ -65,54 +71,39 @@ configureI18n({
|
||||
|
||||
beforeEach(() => {
|
||||
analytics.sendTrackingLogEvent.mockReset();
|
||||
useNavigate.mockReset();
|
||||
});
|
||||
|
||||
const ProfileWrapper = ({ params, requiresParentalConsent }) => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<ProfilePage
|
||||
{...requiredProfilePageProps}
|
||||
params={params}
|
||||
requiresParentalConsent={requiresParentalConsent}
|
||||
navigate={navigate}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ProfileWrapper.propTypes = {
|
||||
params: PropTypes.shape({}).isRequired,
|
||||
requiresParentalConsent: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const ProfilePageWrapper = ({
|
||||
contextValue, store, params, requiresParentalConsent,
|
||||
contextValue, store, params,
|
||||
}) => (
|
||||
<AppContext.Provider
|
||||
value={contextValue}
|
||||
>
|
||||
<AppContext.Provider value={contextValue}>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<ProfileWrapper
|
||||
params={params}
|
||||
requiresParentalConsent={requiresParentalConsent}
|
||||
/>
|
||||
</BrowserRouter>
|
||||
<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' },
|
||||
requiresParentalConsent: null,
|
||||
};
|
||||
|
||||
ProfilePageWrapper.propTypes = {
|
||||
contextValue: PropTypes.shape({}).isRequired,
|
||||
store: PropTypes.shape({}).isRequired,
|
||||
params: PropTypes.shape({}),
|
||||
requiresParentalConsent: PropTypes.bool,
|
||||
params: PropTypes.shape({
|
||||
username: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
describe('<ProfilePage />', () => {
|
||||
@@ -122,17 +113,12 @@ describe('<ProfilePage />', () => {
|
||||
authenticatedUser: { userId: null, username: null, administrator: false },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.loadingApp)} />;
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('successfully redirected to not found page.', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.invalidUser)} />;
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.loadingApp)}
|
||||
/>
|
||||
);
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
@@ -142,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();
|
||||
});
|
||||
@@ -152,7 +143,6 @@ describe('<ProfilePage />', () => {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
@@ -162,105 +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
|
||||
/>
|
||||
);
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('while saving an edited bio', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.savingEditedBio)}
|
||||
/>
|
||||
);
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('while saving an edited bio with error', () => {
|
||||
const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
|
||||
storeData.profilePage.errors.bio = { userMessage: 'bio error' };
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeData)}
|
||||
/>
|
||||
);
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('test country edit with error', () => {
|
||||
const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
|
||||
storeData.profilePage.errors.country = { userMessage: 'country error' };
|
||||
storeData.profilePage.currentlyEditingField = 'country';
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeData)}
|
||||
/>
|
||||
);
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('test education edit with error', () => {
|
||||
const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
|
||||
storeData.profilePage.errors.levelOfEducation = { userMessage: 'education error' };
|
||||
storeData.profilePage.currentlyEditingField = 'levelOfEducation';
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeData)}
|
||||
/>
|
||||
);
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('test preferreded language edit with error', () => {
|
||||
const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
|
||||
storeData.profilePage.errors.languageProficiencies = { userMessage: 'preferred language error' };
|
||||
storeData.profilePage.currentlyEditingField = 'languageProficiencies';
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeData)}
|
||||
params={{ username: 'verified' }}
|
||||
/>
|
||||
);
|
||||
const { container: tree } = render(component);
|
||||
@@ -284,63 +195,24 @@ describe('<ProfilePage />', () => {
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('test age message alert', () => {
|
||||
const storeData = JSON.parse(JSON.stringify(storeMocks.viewOwnProfile));
|
||||
storeData.userAccount.requiresParentalConsent = true;
|
||||
storeData.profilePage.account.requiresParentalConsent = true;
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: { ...getConfig(), COLLECT_YEAR_OF_BIRTH: true },
|
||||
};
|
||||
const { container } = render(
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeData)}
|
||||
requiresParentalConsent
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.alert-info')).toHaveClass('show');
|
||||
});
|
||||
it('test photo error alert', () => {
|
||||
const storeData = JSON.parse(JSON.stringify(storeMocks.viewOwnProfile));
|
||||
storeData.profilePage.errors.photo = { userMessage: 'error' };
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: { ...getConfig(), COLLECT_YEAR_OF_BIRTH: true },
|
||||
};
|
||||
const { container } = render(
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeData)}
|
||||
requiresParentalConsent
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.alert-danger')).toHaveClass('show');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['test user with non-disabled country', 'PK'],
|
||||
['test user with disabled country', 'RU'],
|
||||
])('test user with %s', (_, accountCountry) => {
|
||||
const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
|
||||
storeData.profilePage.errors.country = {};
|
||||
storeData.profilePage.currentlyEditingField = 'country';
|
||||
storeData.profilePage.disabledCountries = ['RU'];
|
||||
storeData.profilePage.account.country = accountCountry;
|
||||
it('successfully redirected to not found page', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const navigate = jest.fn();
|
||||
useNavigate.mockReturnValue(navigate);
|
||||
const component = (
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeData)}
|
||||
store={mockStore(storeMocks.invalidUser)}
|
||||
params={{ username: 'staffTest' }}
|
||||
/>
|
||||
);
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
expect(navigate).toHaveBeenCalledWith('/notfound');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -358,11 +230,30 @@ 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { VisibilityOff } from '@openedx/paragon/icons';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
const UsernameDescription = () => (
|
||||
<div className="d-flex align-items-center mt-3 mb-2rem">
|
||||
<Icon src={VisibilityOff} className="icon-visibility-off" />
|
||||
<div className="username-description">
|
||||
<FormattedMessage
|
||||
id="profile.username.description"
|
||||
defaultMessage="Your profile 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,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default UsernameDescription;
|
||||
@@ -29,7 +29,7 @@ module.exports = {
|
||||
drafts: {},
|
||||
isLoadingProfile: false,
|
||||
isAuthenticatedUserProfile: true,
|
||||
countriesCodesList: [],
|
||||
countriesCodesList: ['US', 'CA', 'GB', 'ME']
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
|
||||
@@ -29,6 +29,7 @@ module.exports = {
|
||||
drafts: {},
|
||||
isLoadingProfile: true,
|
||||
isAuthenticatedUserProfile: true,
|
||||
countriesCodesList: ['US', 'CA', 'GB', 'ME']
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
|
||||
@@ -126,7 +126,7 @@ module.exports = {
|
||||
],
|
||||
drafts: {},
|
||||
isLoadingProfile: false,
|
||||
countriesCodesList: [],
|
||||
disabledCountries: [],
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
|
||||
@@ -81,12 +81,18 @@ module.exports = {
|
||||
gender: null,
|
||||
accountPrivacy: 'private'
|
||||
},
|
||||
preferences: {},
|
||||
preferences: {
|
||||
visibilityName: 'all_users',
|
||||
visibilityCountry: 'all_users',
|
||||
visibilityLevelOfEducation: 'all_users',
|
||||
visibilityLanguageProficiencies: 'all_users',
|
||||
visibilitySocialLinks: 'all_users',
|
||||
visibilityBio: 'all_users'
|
||||
},
|
||||
courseCertificates: [],
|
||||
drafts: {},
|
||||
isLoadingProfile: false,
|
||||
learningGoal: 'advance_career',
|
||||
countriesCodesList: [],
|
||||
countriesCodesList: ['US', 'CA', 'GB', 'ME']
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
|
||||
@@ -124,9 +124,9 @@ module.exports = {
|
||||
createdDate: '2019-03-04T19:31:39.896806Z'
|
||||
}
|
||||
],
|
||||
countriesCodesList:[{code:"AX"},{code:"AL"}],
|
||||
drafts: {},
|
||||
isLoadingProfile: false
|
||||
isLoadingProfile: false,
|
||||
countriesCodesList: ['US', 'CA', 'GB', 'ME']
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,6 @@ export const CLOSE_FORM = 'CLOSE_FORM';
|
||||
export const UPDATE_DRAFT = 'UPDATE_DRAFT';
|
||||
export const RESET_DRAFTS = 'RESET_DRAFTS';
|
||||
|
||||
// FETCH PROFILE ACTIONS
|
||||
|
||||
export const fetchProfile = username => ({
|
||||
type: FETCH_PROFILE.BASE,
|
||||
payload: { username },
|
||||
@@ -25,22 +23,18 @@ export const fetchProfileSuccess = (
|
||||
preferences,
|
||||
courseCertificates,
|
||||
isAuthenticatedUserProfile,
|
||||
countriesCodesList,
|
||||
) => ({
|
||||
type: FETCH_PROFILE.SUCCESS,
|
||||
account,
|
||||
preferences,
|
||||
courseCertificates,
|
||||
isAuthenticatedUserProfile,
|
||||
countriesCodesList,
|
||||
});
|
||||
|
||||
export const fetchProfileReset = () => ({
|
||||
type: FETCH_PROFILE.RESET,
|
||||
});
|
||||
|
||||
// SAVE PROFILE ACTIONS
|
||||
|
||||
export const saveProfile = (formId, username) => ({
|
||||
type: SAVE_PROFILE.BASE,
|
||||
payload: {
|
||||
@@ -70,8 +64,6 @@ export const saveProfileFailure = errors => ({
|
||||
payload: { errors },
|
||||
});
|
||||
|
||||
// SAVE PROFILE PHOTO ACTIONS
|
||||
|
||||
export const saveProfilePhoto = (username, formData) => ({
|
||||
type: SAVE_PROFILE_PHOTO.BASE,
|
||||
payload: {
|
||||
@@ -98,8 +90,6 @@ export const saveProfilePhotoFailure = error => ({
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
// DELETE PROFILE PHOTO ACTIONS
|
||||
|
||||
export const deleteProfilePhoto = username => ({
|
||||
type: DELETE_PROFILE_PHOTO.BASE,
|
||||
payload: {
|
||||
@@ -120,8 +110,6 @@ export const deleteProfilePhotoReset = () => ({
|
||||
type: DELETE_PROFILE_PHOTO.RESET,
|
||||
});
|
||||
|
||||
// FIELD STATE ACTIONS
|
||||
|
||||
export const openForm = formId => ({
|
||||
type: OPEN_FORM,
|
||||
payload: {
|
||||
@@ -136,8 +124,6 @@ export const closeForm = formId => ({
|
||||
},
|
||||
});
|
||||
|
||||
// FORM STATE ACTIONS
|
||||
|
||||
export const updateDraft = (name, value) => ({
|
||||
type: UPDATE_DRAFT,
|
||||
payload: {
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
import {
|
||||
openForm,
|
||||
closeForm,
|
||||
OPEN_FORM,
|
||||
CLOSE_FORM,
|
||||
SAVE_PROFILE,
|
||||
saveProfileBegin,
|
||||
saveProfileSuccess,
|
||||
saveProfileFailure,
|
||||
saveProfileReset,
|
||||
saveProfile,
|
||||
SAVE_PROFILE_PHOTO,
|
||||
saveProfilePhotoBegin,
|
||||
saveProfilePhotoSuccess,
|
||||
@@ -22,76 +12,6 @@ import {
|
||||
deleteProfilePhoto,
|
||||
} from './actions';
|
||||
|
||||
describe('editable field actions', () => {
|
||||
it('should create an open action', () => {
|
||||
const expectedAction = {
|
||||
type: OPEN_FORM,
|
||||
payload: {
|
||||
formId: 'name',
|
||||
},
|
||||
};
|
||||
expect(openForm('name')).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create a closed action', () => {
|
||||
const expectedAction = {
|
||||
type: CLOSE_FORM,
|
||||
payload: {
|
||||
formId: 'name',
|
||||
},
|
||||
};
|
||||
expect(closeForm('name')).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SAVE profile actions', () => {
|
||||
it('should create an action to signal the start of a profile save', () => {
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE.BASE,
|
||||
payload: {
|
||||
formId: 'name',
|
||||
},
|
||||
};
|
||||
expect(saveProfile('name')).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile save success', () => {
|
||||
const accountData = { name: 'Full Name' };
|
||||
const preferencesData = { visibility: { name: 'private' } };
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE.SUCCESS,
|
||||
payload: {
|
||||
account: accountData,
|
||||
preferences: preferencesData,
|
||||
},
|
||||
};
|
||||
expect(saveProfileSuccess(accountData, preferencesData)).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile save beginning', () => {
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE.BEGIN,
|
||||
};
|
||||
expect(saveProfileBegin()).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile save success', () => {
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE.RESET,
|
||||
};
|
||||
expect(saveProfileReset()).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user account save failure', () => {
|
||||
const errors = ['Test failure'];
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE.FAILURE,
|
||||
payload: { errors },
|
||||
};
|
||||
expect(saveProfileFailure(errors)).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SAVE profile photo actions', () => {
|
||||
it('should create an action to signal the start of a profile photo save', () => {
|
||||
const formData = 'multipart form data';
|
||||
@@ -123,7 +43,7 @@ describe('SAVE profile photo actions', () => {
|
||||
expect(saveProfilePhotoSuccess(newPhotoData)).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo save success', () => {
|
||||
it('should create an action to signal user profile photo save reset', () => {
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE_PHOTO.RESET,
|
||||
};
|
||||
@@ -169,34 +89,10 @@ describe('DELETE profile photo actions', () => {
|
||||
expect(deleteProfilePhotoSuccess(defaultPhotoData)).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo deletion success', () => {
|
||||
it('should create an action to signal user profile photo deletion reset', () => {
|
||||
const expectedAction = {
|
||||
type: DELETE_PROFILE_PHOTO.RESET,
|
||||
};
|
||||
expect(deleteProfilePhotoReset()).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Editable field opening and closing actions', () => {
|
||||
const formId = 'name';
|
||||
|
||||
it('should create an action to signal the opening a field', () => {
|
||||
const expectedAction = {
|
||||
type: OPEN_FORM,
|
||||
payload: {
|
||||
formId,
|
||||
},
|
||||
};
|
||||
expect(openForm(formId)).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal the closing a field', () => {
|
||||
const expectedAction = {
|
||||
type: CLOSE_FORM,
|
||||
payload: {
|
||||
formId,
|
||||
},
|
||||
};
|
||||
expect(closeForm(formId)).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ export function useIsOnMobileScreen() {
|
||||
}
|
||||
|
||||
export function useIsVisibilityEnabled() {
|
||||
return getConfig().DISABLE_VISIBILITY_EDITING === 'true';
|
||||
return getConfig().DISABLE_VISIBILITY_EDITING !== 'true';
|
||||
}
|
||||
|
||||
export function useHandleChange(changeHandler) {
|
||||
@@ -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({
|
||||
|
||||
@@ -16,12 +16,27 @@ export const initialState = {
|
||||
currentlyEditingField: null,
|
||||
account: {
|
||||
socialLinks: [],
|
||||
languageProficiencies: [],
|
||||
name: '',
|
||||
bio: '',
|
||||
country: '',
|
||||
levelOfEducation: '',
|
||||
profileImage: {},
|
||||
yearOfBirth: '',
|
||||
},
|
||||
preferences: {
|
||||
visibilityName: '',
|
||||
visibilityBio: '',
|
||||
visibilityCountry: '',
|
||||
visibilityLevelOfEducation: '',
|
||||
visibilitySocialLinks: '',
|
||||
visibilityLanguageProficiencies: '',
|
||||
},
|
||||
preferences: {},
|
||||
courseCertificates: [],
|
||||
drafts: {},
|
||||
isLoadingProfile: true,
|
||||
isAuthenticatedUserProfile: false,
|
||||
disabledCountries: ['RU'],
|
||||
countriesCodesList: [],
|
||||
};
|
||||
|
||||
@@ -38,12 +53,17 @@ const profilePage = (state = initialState, action = {}) => {
|
||||
case FETCH_PROFILE.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
account: action.account,
|
||||
account: {
|
||||
...state.account,
|
||||
...action.account,
|
||||
socialLinks: action.account.socialLinks || [],
|
||||
languageProficiencies: action.account.languageProficiencies || [],
|
||||
},
|
||||
preferences: action.preferences,
|
||||
courseCertificates: action.courseCertificates,
|
||||
courseCertificates: action.courseCertificates || [],
|
||||
isLoadingProfile: false,
|
||||
isAuthenticatedUserProfile: action.isAuthenticatedUserProfile,
|
||||
countriesCodesList: action.countriesCodesList,
|
||||
countriesCodesList: action.countriesCodesList || [],
|
||||
};
|
||||
case SAVE_PROFILE.BEGIN:
|
||||
return {
|
||||
@@ -56,9 +76,12 @@ const profilePage = (state = initialState, action = {}) => {
|
||||
...state,
|
||||
saveState: 'complete',
|
||||
errors: {},
|
||||
// Account is always replaced completely.
|
||||
account: action.payload.account !== null ? action.payload.account : state.account,
|
||||
// Preferences changes get merged in.
|
||||
account: action.payload.account !== null ? {
|
||||
...state.account,
|
||||
...action.payload.account,
|
||||
socialLinks: action.payload.account.socialLinks || [],
|
||||
languageProficiencies: action.payload.account.languageProficiencies || [],
|
||||
} : state.account,
|
||||
preferences: { ...state.preferences, ...action.payload.preferences },
|
||||
};
|
||||
case SAVE_PROFILE.FAILURE:
|
||||
@@ -75,7 +98,6 @@ const profilePage = (state = initialState, action = {}) => {
|
||||
isLoadingProfile: false,
|
||||
errors: {},
|
||||
};
|
||||
|
||||
case SAVE_PROFILE_PHOTO.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
@@ -85,7 +107,6 @@ const profilePage = (state = initialState, action = {}) => {
|
||||
case SAVE_PROFILE_PHOTO.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
// Merge in new profile image data
|
||||
account: { ...state.account, profileImage: action.payload.profileImage },
|
||||
savePhotoState: 'complete',
|
||||
errors: {},
|
||||
@@ -102,7 +123,6 @@ const profilePage = (state = initialState, action = {}) => {
|
||||
savePhotoState: null,
|
||||
errors: {},
|
||||
};
|
||||
|
||||
case DELETE_PROFILE_PHOTO.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
@@ -112,7 +132,6 @@ const profilePage = (state = initialState, action = {}) => {
|
||||
case DELETE_PROFILE_PHOTO.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
// Merge in new profile image data (should be empty or default image)
|
||||
account: { ...state.account, profileImage: action.payload.profileImage },
|
||||
savePhotoState: 'complete',
|
||||
errors: {},
|
||||
@@ -129,13 +148,11 @@ const profilePage = (state = initialState, action = {}) => {
|
||||
savePhotoState: null,
|
||||
errors: {},
|
||||
};
|
||||
|
||||
case UPDATE_DRAFT:
|
||||
return {
|
||||
...state,
|
||||
drafts: { ...state.drafts, [action.payload.name]: action.payload.value },
|
||||
};
|
||||
|
||||
case RESET_DRAFTS:
|
||||
return {
|
||||
...state,
|
||||
@@ -148,7 +165,6 @@ const profilePage = (state = initialState, action = {}) => {
|
||||
drafts: {},
|
||||
};
|
||||
case CLOSE_FORM:
|
||||
// Only close if the field to close is undefined or matches the field that is currently open
|
||||
if (action.payload.formId === state.currentlyEditingField) {
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import pick from 'lodash.pick';
|
||||
import {
|
||||
@@ -21,13 +22,12 @@ import {
|
||||
resetDrafts,
|
||||
saveProfileBegin,
|
||||
saveProfileFailure,
|
||||
saveProfilePhotoBegin,
|
||||
saveProfilePhotoFailure,
|
||||
saveProfilePhotoReset,
|
||||
saveProfilePhotoSuccess,
|
||||
saveProfileReset,
|
||||
saveProfileSuccess,
|
||||
SAVE_PROFILE,
|
||||
saveProfilePhotoBegin,
|
||||
saveProfilePhotoReset,
|
||||
saveProfilePhotoSuccess,
|
||||
SAVE_PROFILE_PHOTO,
|
||||
} from './actions';
|
||||
import { handleSaveProfileSelector, userAccountSelector } from './selectors';
|
||||
@@ -37,7 +37,6 @@ export function* handleFetchProfile(action) {
|
||||
const { username } = action.payload;
|
||||
const userAccount = yield select(userAccountSelector);
|
||||
const isAuthenticatedUserProfile = username === getAuthenticatedUser().username;
|
||||
// Default our data assuming the account is the current user's account.
|
||||
let preferences = {};
|
||||
let account = userAccount;
|
||||
let courseCertificates = null;
|
||||
@@ -46,7 +45,6 @@ export function* handleFetchProfile(action) {
|
||||
try {
|
||||
yield put(fetchProfileBegin());
|
||||
|
||||
// Depending on which profile we're loading, we need to make different calls.
|
||||
const calls = [
|
||||
call(ProfileApiService.getAccount, username),
|
||||
call(ProfileApiService.getCourseCertificates, username),
|
||||
@@ -54,12 +52,9 @@ export function* handleFetchProfile(action) {
|
||||
];
|
||||
|
||||
if (isAuthenticatedUserProfile) {
|
||||
// If the profile is for the current user, get their preferences.
|
||||
// We don't need them for other users.
|
||||
calls.push(call(ProfileApiService.getPreferences, username));
|
||||
}
|
||||
|
||||
// Make all the calls in parallel.
|
||||
const result = yield all(calls);
|
||||
|
||||
if (isAuthenticatedUserProfile) {
|
||||
@@ -68,9 +63,6 @@ export function* handleFetchProfile(action) {
|
||||
[account, courseCertificates, countriesCodesList] = result;
|
||||
}
|
||||
|
||||
// Set initial visibility values for account
|
||||
// Set account_privacy as custom is necessary so that when viewing another user's profile,
|
||||
// their full name is displayed and change visibility forms are worked correctly
|
||||
if (isAuthenticatedUserProfile && result[0].accountPrivacy === 'all_users') {
|
||||
yield call(ProfileApiService.patchPreferences, action.payload.username, {
|
||||
account_privacy: 'custom',
|
||||
@@ -97,11 +89,7 @@ export function* handleFetchProfile(action) {
|
||||
yield put(fetchProfileReset());
|
||||
} catch (e) {
|
||||
if (e.response.status === 404) {
|
||||
if (e.processedData && e.processedData.fieldErrors) {
|
||||
yield put(saveProfileFailure(e.processedData.fieldErrors));
|
||||
} else {
|
||||
yield put(saveProfileFailure(e.customAttributes));
|
||||
}
|
||||
history.push('/notfound');
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
@@ -114,7 +102,6 @@ export function* handleSaveProfile(action) {
|
||||
|
||||
const accountDrafts = pick(drafts, [
|
||||
'bio',
|
||||
'courseCertificates',
|
||||
'country',
|
||||
'levelOfEducation',
|
||||
'languageProficiencies',
|
||||
@@ -124,7 +111,6 @@ export function* handleSaveProfile(action) {
|
||||
|
||||
const preferencesDrafts = pick(drafts, [
|
||||
'visibilityBio',
|
||||
'visibilityCourseCertificates',
|
||||
'visibilityCountry',
|
||||
'visibilityLevelOfEducation',
|
||||
'visibilityLanguageProficiencies',
|
||||
@@ -138,7 +124,6 @@ export function* handleSaveProfile(action) {
|
||||
|
||||
yield put(saveProfileBegin());
|
||||
let accountResult = null;
|
||||
// Build the visibility drafts into a structure the API expects.
|
||||
|
||||
if (Object.keys(accountDrafts).length > 0) {
|
||||
accountResult = yield call(
|
||||
@@ -148,17 +133,14 @@ export function* handleSaveProfile(action) {
|
||||
);
|
||||
}
|
||||
|
||||
let preferencesResult = preferences; // assume it hasn't changed.
|
||||
let preferencesResult = preferences;
|
||||
if (Object.keys(preferencesDrafts).length > 0) {
|
||||
yield call(ProfileApiService.patchPreferences, action.payload.username, preferencesDrafts);
|
||||
// TODO: Temporary deoptimization since the patchPreferences call doesn't return anything.
|
||||
// Remove this second call once we can get a result from the one above.
|
||||
|
||||
preferencesResult = yield call(ProfileApiService.getPreferences, action.payload.username);
|
||||
}
|
||||
|
||||
// The account result is returned from the server.
|
||||
// The preferences draft is valid if the server didn't complain, so
|
||||
// pass it through directly.
|
||||
yield put(saveProfileSuccess(accountResult, preferencesResult));
|
||||
yield delay(1000);
|
||||
yield put(closeForm(action.payload.formId));
|
||||
@@ -184,12 +166,7 @@ export function* handleSaveProfilePhoto(action) {
|
||||
yield put(saveProfilePhotoSuccess(photoResult));
|
||||
yield put(saveProfilePhotoReset());
|
||||
} catch (e) {
|
||||
if (e.processedData) {
|
||||
yield put(saveProfilePhotoFailure(e.processedData));
|
||||
} else {
|
||||
yield put(saveProfilePhotoReset());
|
||||
throw e;
|
||||
}
|
||||
yield put(saveProfilePhotoReset());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +180,6 @@ export function* handleDeleteProfilePhoto(action) {
|
||||
yield put(deleteProfilePhotoReset());
|
||||
} catch (e) {
|
||||
yield put(deleteProfilePhotoReset());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedUser: jest.fn(),
|
||||
}));
|
||||
|
||||
// RootSaga and ProfileApiService must be imported AFTER the mock above.
|
||||
/* eslint-disable import/first */
|
||||
import profileSaga, {
|
||||
handleFetchProfile,
|
||||
@@ -78,7 +77,6 @@ describe('RootSaga', () => {
|
||||
call(ProfileApiService.getCourseCertificates, 'gonzo'),
|
||||
call(ProfileApiService.getCountryList),
|
||||
call(ProfileApiService.getPreferences, 'gonzo'),
|
||||
|
||||
]));
|
||||
expect(gen.next(result).value)
|
||||
.toEqual(put(profileActions.fetchProfileSuccess(userAccount, result[3], result[1], true, [])));
|
||||
@@ -137,8 +135,6 @@ describe('RootSaga', () => {
|
||||
expect(gen.next().value).toEqual(call(ProfileApiService.patchProfile, 'my username', {
|
||||
name: 'Full Name',
|
||||
}));
|
||||
// The library would supply the result of the above call
|
||||
// as the parameter to the NEXT yield. Here:
|
||||
expect(gen.next(profile).value).toEqual(put(profileActions.saveProfileSuccess(profile, {})));
|
||||
expect(gen.next().value).toEqual(delay(1000));
|
||||
expect(gen.next().value).toEqual(put(profileActions.closeForm('ze form id')));
|
||||
|
||||
@@ -5,18 +5,15 @@ import {
|
||||
getCountryList,
|
||||
getCountryMessages,
|
||||
getLanguageMessages,
|
||||
} from '@edx/frontend-platform/i18n'; // eslint-disable-line
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
export const formIdSelector = (state, props) => props.formId;
|
||||
export const userAccountSelector = state => state.userAccount;
|
||||
|
||||
export const profileAccountSelector = state => state.profilePage.account;
|
||||
export const profileDraftsSelector = state => state.profilePage.drafts;
|
||||
export const accountPrivacySelector = state => state.profilePage.preferences.accountPrivacy;
|
||||
export const profilePreferencesSelector = state => state.profilePage.preferences;
|
||||
export const profileCourseCertificatesSelector = state => state.profilePage.courseCertificates;
|
||||
export const profileAccountDraftsSelector = state => state.profilePage.accountDrafts;
|
||||
export const profileVisibilityDraftsSelector = state => state.profilePage.visibilityDrafts;
|
||||
export const saveStateSelector = state => state.profilePage.saveState;
|
||||
export const savePhotoStateSelector = state => state.profilePage.savePhotoState;
|
||||
export const isLoadingProfileSelector = state => state.profilePage.isLoadingProfile;
|
||||
@@ -32,22 +29,11 @@ export const editableFormModeSelector = createSelector(
|
||||
formIdSelector,
|
||||
currentlyEditingFieldSelector,
|
||||
(account, isAuthenticatedUserProfile, certificates, formId, currentlyEditingField) => {
|
||||
// If the prop doesn't exist, that means it hasn't been set (for the current user's profile)
|
||||
// or is being hidden from us (for other users' profiles)
|
||||
let propExists = account[formId] != null && account[formId].length > 0;
|
||||
propExists = formId === 'certificates' ? certificates.length > 0 : propExists; // overwrite for certificates
|
||||
// If this isn't the current user's profile
|
||||
propExists = formId === 'certificates' ? certificates.length > 0 : propExists;
|
||||
if (!isAuthenticatedUserProfile) {
|
||||
return 'static';
|
||||
}
|
||||
// the current user has no age set / under 13 ...
|
||||
if (account.requiresParentalConsent) {
|
||||
// then there are only two options: static or nothing.
|
||||
// We use 'null' as a return value because the consumers of
|
||||
// getMode render nothing at all on a mode of null.
|
||||
return propExists ? 'static' : null;
|
||||
}
|
||||
// Otherwise, if this is the current user's profile...
|
||||
if (formId === currentlyEditingField) {
|
||||
return 'editing';
|
||||
}
|
||||
@@ -68,12 +54,10 @@ export const accountDraftsFieldSelector = createSelector(
|
||||
|
||||
export const visibilityDraftsFieldSelector = createSelector(
|
||||
formIdSelector,
|
||||
profileVisibilityDraftsSelector,
|
||||
(formId, visibilityDrafts) => visibilityDrafts[formId],
|
||||
profileDraftsSelector,
|
||||
(formId, drafts) => drafts[`visibility${formId.charAt(0).toUpperCase() + formId.slice(1)}`],
|
||||
);
|
||||
|
||||
// Note: Error messages are delivered from the server
|
||||
// localized according to a user's account settings
|
||||
export const formErrorSelector = createSelector(
|
||||
accountErrorsSelector,
|
||||
formIdSelector,
|
||||
@@ -91,11 +75,6 @@ export const editableFormSelector = createSelector(
|
||||
}),
|
||||
);
|
||||
|
||||
// Because this selector has no input selectors, it will only be evaluated once. This is fine
|
||||
// for now because we don't allow users to change the locale after page load.
|
||||
// Once we DO allow this, we should create an actual action which dispatches the locale into redux,
|
||||
// then we can modify this to get the locale from state rather than from getLocale() directly.
|
||||
// Once we do that, this will work as expected and be re-evaluated when the locale changes.
|
||||
export const localeSelector = () => getLocale();
|
||||
export const countryMessagesSelector = createSelector(
|
||||
localeSelector,
|
||||
@@ -169,9 +148,6 @@ export const profileImageSelector = createSelector(
|
||||
: {}),
|
||||
);
|
||||
|
||||
/**
|
||||
* This is used by a saga to pull out data to process.
|
||||
*/
|
||||
export const handleSaveProfileSelector = createSelector(
|
||||
profileDraftsSelector,
|
||||
profilePreferencesSelector,
|
||||
@@ -181,7 +157,6 @@ export const handleSaveProfileSelector = createSelector(
|
||||
}),
|
||||
);
|
||||
|
||||
// Reformats the social links in a platform-keyed hash.
|
||||
const socialLinksByPlatformSelector = createSelector(
|
||||
profileAccountSelector,
|
||||
(account) => {
|
||||
@@ -208,24 +183,18 @@ const draftSocialLinksByPlatformSelector = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
// Fleshes out our list of existing social links with all the other ones the user can set.
|
||||
export const formSocialLinksSelector = createSelector(
|
||||
socialLinksByPlatformSelector,
|
||||
draftSocialLinksByPlatformSelector,
|
||||
(linksByPlatform, draftLinksByPlatform) => {
|
||||
const knownPlatforms = ['twitter', 'facebook', 'linkedin'];
|
||||
const socialLinks = [];
|
||||
// For each known platform
|
||||
knownPlatforms.forEach((platform) => {
|
||||
// If the link is in our drafts.
|
||||
if (draftLinksByPlatform[platform] !== undefined) {
|
||||
// Use the draft one.
|
||||
socialLinks.push(draftLinksByPlatform[platform]);
|
||||
} else if (linksByPlatform[platform] !== undefined) {
|
||||
// Otherwise use the real one.
|
||||
socialLinks.push(linksByPlatform[platform]);
|
||||
} else {
|
||||
// And if it's not in either, use a stub.
|
||||
socialLinks.push({
|
||||
platform,
|
||||
socialLink: null,
|
||||
@@ -244,7 +213,6 @@ export const visibilitiesSelector = createSelector(
|
||||
case 'custom':
|
||||
return {
|
||||
visibilityBio: preferences.visibilityBio || 'all_users',
|
||||
visibilityCourseCertificates: preferences.visibilityCourseCertificates || 'all_users',
|
||||
visibilityCountry: preferences.visibilityCountry || 'all_users',
|
||||
visibilityLevelOfEducation: preferences.visibilityLevelOfEducation || 'all_users',
|
||||
visibilityLanguageProficiencies: preferences.visibilityLanguageProficiencies || 'all_users',
|
||||
@@ -254,7 +222,6 @@ export const visibilitiesSelector = createSelector(
|
||||
case 'private':
|
||||
return {
|
||||
visibilityBio: 'private',
|
||||
visibilityCourseCertificates: 'private',
|
||||
visibilityCountry: 'private',
|
||||
visibilityLevelOfEducation: 'private',
|
||||
visibilityLanguageProficiencies: 'private',
|
||||
@@ -263,13 +230,8 @@ export const visibilitiesSelector = createSelector(
|
||||
};
|
||||
case 'all_users':
|
||||
default:
|
||||
// All users is intended to fall through to default.
|
||||
// If there is no value for accountPrivacy in perferences, that means it has not been
|
||||
// explicitly set yet. The server assumes - today - that this means "all_users",
|
||||
// so we emulate that here in the client.
|
||||
return {
|
||||
visibilityBio: 'all_users',
|
||||
visibilityCourseCertificates: 'all_users',
|
||||
visibilityCountry: 'all_users',
|
||||
visibilityLevelOfEducation: 'all_users',
|
||||
visibilityLanguageProficiencies: 'all_users',
|
||||
@@ -280,9 +242,6 @@ export const visibilitiesSelector = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* If there's no draft present at all (undefined), use the original committed value.
|
||||
*/
|
||||
function chooseFormValue(draft, committed) {
|
||||
return draft !== undefined ? draft : committed;
|
||||
}
|
||||
@@ -297,10 +256,6 @@ export const formValuesSelector = createSelector(
|
||||
bio: chooseFormValue(drafts.bio, account.bio),
|
||||
visibilityBio: chooseFormValue(drafts.visibilityBio, visibilities.visibilityBio),
|
||||
courseCertificates,
|
||||
visibilityCourseCertificates: chooseFormValue(
|
||||
drafts.visibilityCourseCertificates,
|
||||
visibilities.visibilityCourseCertificates,
|
||||
),
|
||||
country: chooseFormValue(drafts.country, account.country),
|
||||
visibilityCountry: chooseFormValue(drafts.visibilityCountry, visibilities.visibilityCountry),
|
||||
levelOfEducation: chooseFormValue(drafts.levelOfEducation, account.levelOfEducation),
|
||||
@@ -318,7 +273,7 @@ export const formValuesSelector = createSelector(
|
||||
),
|
||||
name: chooseFormValue(drafts.name, account.name),
|
||||
visibilityName: chooseFormValue(drafts.visibilityName, visibilities.visibilityName),
|
||||
socialLinks, // Social links is calculated in its own selector, since it's complicated.
|
||||
socialLinks,
|
||||
visibilitySocialLinks: chooseFormValue(
|
||||
drafts.visibilitySocialLinks,
|
||||
visibilities.visibilitySocialLinks,
|
||||
@@ -335,6 +290,7 @@ export const profilePageSelector = createSelector(
|
||||
isLoadingProfileSelector,
|
||||
draftSocialLinksByPlatformSelector,
|
||||
accountErrorsSelector,
|
||||
isAuthenticatedUserProfileSelector,
|
||||
(
|
||||
account,
|
||||
formValues,
|
||||
@@ -344,47 +300,39 @@ export const profilePageSelector = createSelector(
|
||||
isLoadingProfile,
|
||||
draftSocialLinksByPlatform,
|
||||
errors,
|
||||
isAuthenticatedUserProfile,
|
||||
) => ({
|
||||
// Account data we need
|
||||
username: account.username,
|
||||
profileImage,
|
||||
requiresParentalConsent: account.requiresParentalConsent,
|
||||
dateJoined: account.dateJoined,
|
||||
yearOfBirth: account.yearOfBirth,
|
||||
|
||||
// Bio form data
|
||||
bio: formValues.bio,
|
||||
visibilityBio: formValues.visibilityBio,
|
||||
|
||||
// Certificates form data
|
||||
courseCertificates: formValues.courseCertificates,
|
||||
visibilityCourseCertificates: formValues.visibilityCourseCertificates,
|
||||
|
||||
// Country form data
|
||||
country: formValues.country,
|
||||
visibilityCountry: formValues.visibilityCountry,
|
||||
|
||||
// Education form data
|
||||
levelOfEducation: formValues.levelOfEducation,
|
||||
visibilityLevelOfEducation: formValues.visibilityLevelOfEducation,
|
||||
|
||||
// Language proficiency form data
|
||||
languageProficiencies: formValues.languageProficiencies,
|
||||
visibilityLanguageProficiencies: formValues.visibilityLanguageProficiencies,
|
||||
|
||||
// Name form data
|
||||
name: formValues.name,
|
||||
visibilityName: formValues.visibilityName,
|
||||
|
||||
// Social links form data
|
||||
socialLinks: formValues.socialLinks,
|
||||
visibilitySocialLinks: formValues.visibilitySocialLinks,
|
||||
draftSocialLinksByPlatform,
|
||||
|
||||
// Other data we need
|
||||
saveState,
|
||||
savePhotoState,
|
||||
isLoadingProfile,
|
||||
photoUploadError: errors.photo || null,
|
||||
isAuthenticatedUserProfile,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -7,7 +7,19 @@ import { FIELD_LABELS } from './constants';
|
||||
ensureConfig(['LMS_BASE_URL'], 'Profile API service');
|
||||
|
||||
function processAccountData(data) {
|
||||
return camelCaseObject(data);
|
||||
const processedData = camelCaseObject(data);
|
||||
return {
|
||||
...processedData,
|
||||
socialLinks: Array.isArray(processedData.socialLinks) ? processedData.socialLinks : [],
|
||||
languageProficiencies: Array.isArray(processedData.languageProficiencies)
|
||||
? processedData.languageProficiencies : [],
|
||||
name: processedData.name || null,
|
||||
bio: processedData.bio || null,
|
||||
country: processedData.country || null,
|
||||
levelOfEducation: processedData.levelOfEducation || null,
|
||||
profileImage: processedData.profileImage || {},
|
||||
yearOfBirth: processedData.yearOfBirth || null,
|
||||
};
|
||||
}
|
||||
|
||||
function processAndThrowError(error, errorDataProcessor) {
|
||||
@@ -20,15 +32,12 @@ function processAndThrowError(error, errorDataProcessor) {
|
||||
}
|
||||
}
|
||||
|
||||
// GET ACCOUNT
|
||||
export async function getAccount(username) {
|
||||
const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`);
|
||||
|
||||
// Process response data
|
||||
return processAccountData(data);
|
||||
}
|
||||
|
||||
// PATCH PROFILE
|
||||
export async function patchProfile(username, params) {
|
||||
const processedParams = snakeCaseObject(params);
|
||||
|
||||
@@ -42,12 +51,9 @@ export async function patchProfile(username, params) {
|
||||
processAndThrowError(error, processAccountData);
|
||||
});
|
||||
|
||||
// Process response data
|
||||
return processAccountData(data);
|
||||
}
|
||||
|
||||
// POST PROFILE PHOTO
|
||||
|
||||
export async function postProfilePhoto(username, formData) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { data } = await getHttpClient().post(
|
||||
@@ -71,8 +77,6 @@ export async function postProfilePhoto(username, formData) {
|
||||
return updatedData.profileImage;
|
||||
}
|
||||
|
||||
// DELETE PROFILE PHOTO
|
||||
|
||||
export async function deleteProfilePhoto(username) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { data } = await getHttpClient().delete(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`);
|
||||
@@ -86,14 +90,12 @@ export async function deleteProfilePhoto(username) {
|
||||
return updatedData.profileImage;
|
||||
}
|
||||
|
||||
// GET PREFERENCES
|
||||
export async function getPreferences(username) {
|
||||
const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`);
|
||||
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
// PATCH PREFERENCES
|
||||
export async function patchPreferences(username, params) {
|
||||
let processedParams = snakeCaseObject(params);
|
||||
processedParams = convertKeyNames(processedParams, {
|
||||
@@ -115,8 +117,6 @@ export async function patchPreferences(username, params) {
|
||||
return params; // TODO: Once the server returns the updated preferences object, return that.
|
||||
}
|
||||
|
||||
// GET COURSE CERTIFICATES
|
||||
|
||||
function transformCertificateData(data) {
|
||||
const transformedData = [];
|
||||
data.forEach((cert) => {
|
||||
|
||||
@@ -1,149 +1,140 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import messages from './Bio.messages';
|
||||
|
||||
// Components
|
||||
import FormControls from './elements/FormControls';
|
||||
import EditableItemHeader from './elements/EditableItemHeader';
|
||||
import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Selectors
|
||||
import { editableFormSelector } from '../data/selectors';
|
||||
import {
|
||||
useCloseOpenHandler,
|
||||
useHandleChange,
|
||||
useHandleSubmit,
|
||||
useIsOnMobileScreen,
|
||||
useIsVisibilityEnabled,
|
||||
} from '../data/hooks';
|
||||
|
||||
class Bio extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const Bio = ({
|
||||
formId,
|
||||
bio,
|
||||
visibilityBio,
|
||||
editMode,
|
||||
saveState,
|
||||
error,
|
||||
changeHandler,
|
||||
submitHandler,
|
||||
closeHandler,
|
||||
openHandler,
|
||||
}) => {
|
||||
const isMobileView = useIsOnMobileScreen();
|
||||
const isVisibilityEnabled = useIsVisibilityEnabled();
|
||||
const intl = useIntl();
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.handleOpen = this.handleOpen.bind(this);
|
||||
}
|
||||
const handleChange = useHandleChange(changeHandler);
|
||||
const handleSubmit = useHandleSubmit(submitHandler, formId);
|
||||
const handleOpen = useCloseOpenHandler(openHandler, formId);
|
||||
const handleClose = useCloseOpenHandler(closeHandler, formId);
|
||||
|
||||
handleChange(e) {
|
||||
const { name, value } = e.target;
|
||||
this.props.changeHandler(name, value);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.props.submitHandler(this.props.formId);
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
this.props.closeHandler(this.props.formId);
|
||||
}
|
||||
|
||||
handleOpen() {
|
||||
this.props.openHandler(this.props.formId);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
formId, bio, visibilityBio, editMode, saveState, error, intl,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<SwitchContent
|
||||
className="mb-5"
|
||||
expression={editMode}
|
||||
cases={{
|
||||
editing: (
|
||||
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<Form.Group
|
||||
controlId={formId}
|
||||
isInvalid={error !== null}
|
||||
>
|
||||
<label className="edit-section-header" htmlFor={formId}>
|
||||
{intl.formatMessage(messages['profile.bio.about.me'])}
|
||||
</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
id={formId}
|
||||
name={formId}
|
||||
value={bio}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
{error !== null && (
|
||||
<Form.Control.Feedback hasIcon={false}>
|
||||
{error}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
<FormControls
|
||||
visibilityId="visibilityBio"
|
||||
saveState={saveState}
|
||||
visibility={visibilityBio}
|
||||
cancelHandler={this.handleClose}
|
||||
changeHandler={this.handleChange}
|
||||
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}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
),
|
||||
editable: (
|
||||
<>
|
||||
<EditableItemHeader
|
||||
content={intl.formatMessage(messages['profile.bio.about.me'])}
|
||||
showEditButton
|
||||
onClickEdit={this.handleOpen}
|
||||
showVisibility={visibilityBio !== null}
|
||||
{error !== null && (
|
||||
<Form.Control.Feedback hasIcon={false}>
|
||||
{error}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
<FormControls
|
||||
visibilityId="visibilityBio"
|
||||
saveState={saveState}
|
||||
visibility={visibilityBio}
|
||||
cancelHandler={handleClose}
|
||||
changeHandler={handleChange}
|
||||
/>
|
||||
<p data-hj-suppress className="lead">{bio}</p>
|
||||
</>
|
||||
),
|
||||
empty: (
|
||||
<>
|
||||
<EditableItemHeader content={intl.formatMessage(messages['profile.bio.about.me'])} />
|
||||
<EmptyContent onClick={this.handleOpen}>
|
||||
<FormattedMessage
|
||||
id="profile.bio.empty"
|
||||
defaultMessage="Add a short bio"
|
||||
description="instructions when the user hasn't written an About Me"
|
||||
/>
|
||||
</EmptyContent>
|
||||
</>
|
||||
),
|
||||
static: (
|
||||
<>
|
||||
<EditableItemHeader content={intl.formatMessage(messages['profile.bio.about.me'])} />
|
||||
<p data-hj-suppress className="lead">{bio}</p>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
</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 = {
|
||||
// It'd be nice to just set this as a defaultProps...
|
||||
// except the class that comes out on the other side of react-redux's
|
||||
// connect() method won't have it anymore. Static properties won't survive
|
||||
// through the higher order function.
|
||||
formId: PropTypes.string.isRequired,
|
||||
|
||||
// From Selector
|
||||
bio: PropTypes.string,
|
||||
visibilityBio: PropTypes.oneOf(['private', 'all_users']),
|
||||
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
|
||||
saveState: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
|
||||
// Actions
|
||||
changeHandler: PropTypes.func.isRequired,
|
||||
submitHandler: PropTypes.func.isRequired,
|
||||
closeHandler: PropTypes.func.isRequired,
|
||||
openHandler: PropTypes.func.isRequired,
|
||||
|
||||
// i18n
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
Bio.defaultProps = {
|
||||
@@ -157,4 +148,4 @@ Bio.defaultProps = {
|
||||
export default connect(
|
||||
editableFormSelector,
|
||||
{},
|
||||
)(injectIntl(Bio));
|
||||
)(Bio);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
const messages = defineMessages({
|
||||
'profile.bio.about.me': {
|
||||
id: 'profile.bio.about.me',
|
||||
defaultMessage: 'About Me',
|
||||
defaultMessage: 'Bio',
|
||||
description: 'A section of a user profile',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FormattedDate, FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { connect } from 'react-redux';
|
||||
import get from 'lodash.get';
|
||||
|
||||
import messages from './Certificates.messages';
|
||||
|
||||
// Components
|
||||
import FormControls from './elements/FormControls';
|
||||
import EditableItemHeader from './elements/EditableItemHeader';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Assets
|
||||
import professionalCertificateSVG from '../assets/professional-certificate.svg';
|
||||
import verifiedCertificateSVG from '../assets/verified-certificate.svg';
|
||||
|
||||
// Selectors
|
||||
import { certificatesSelector } from '../data/selectors';
|
||||
|
||||
class Certificates extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.handleOpen = this.handleOpen.bind(this);
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
const { name, value } = e.target;
|
||||
this.props.changeHandler(name, value);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.props.submitHandler(this.props.formId);
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
this.props.closeHandler(this.props.formId);
|
||||
}
|
||||
|
||||
handleOpen() {
|
||||
this.props.openHandler(this.props.formId);
|
||||
}
|
||||
|
||||
renderCertificate({
|
||||
certificateType, courseDisplayName, courseOrganization, modifiedDate, downloadUrl, courseId,
|
||||
}) {
|
||||
const { intl } = this.props;
|
||||
const certificateIllustration = (() => {
|
||||
switch (certificateType) {
|
||||
case 'professional':
|
||||
case 'no-id-professional':
|
||||
return professionalCertificateSVG;
|
||||
case 'verified':
|
||||
return verifiedCertificateSVG;
|
||||
case 'honor':
|
||||
case 'audit':
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<div key={`${modifiedDate}-${courseId}`} className="col-12 col-sm-6 d-flex align-items-stretch">
|
||||
<div className="card mb-4 certificate flex-grow-1">
|
||||
<div
|
||||
className="certificate-type-illustration"
|
||||
style={{ backgroundImage: `url(${certificateIllustration})` }}
|
||||
/>
|
||||
<div className="card-body d-flex flex-column">
|
||||
<div className="card-title">
|
||||
<p className="small mb-0">
|
||||
{intl.formatMessage(get(
|
||||
messages,
|
||||
`profile.certificates.types.${certificateType}`,
|
||||
messages['profile.certificates.types.unknown'],
|
||||
))}
|
||||
</p>
|
||||
<h4 className="certificate-title">{courseDisplayName}</h4>
|
||||
</div>
|
||||
<p className="small mb-0">
|
||||
<FormattedMessage
|
||||
id="profile.certificate.organization.label"
|
||||
defaultMessage="From"
|
||||
/>
|
||||
</p>
|
||||
<p className="h6 mb-4">{courseOrganization}</p>
|
||||
<div className="flex-grow-1" />
|
||||
<p className="small mb-2">
|
||||
<FormattedMessage
|
||||
id="profile.certificate.completion.date.label"
|
||||
defaultMessage="Completed on {date}"
|
||||
values={{
|
||||
date: <FormattedDate value={new Date(modifiedDate)} />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<div>
|
||||
<Hyperlink destination={downloadUrl} className="btn btn-outline-primary" target="_blank">
|
||||
{intl.formatMessage(messages['profile.certificates.view.certificate'])}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderCertificates() {
|
||||
if (this.props.certificates === null || this.props.certificates.length === 0) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="profile.no.certificates"
|
||||
defaultMessage="You don't have any certificates yet."
|
||||
description="displays when user has no course completion certificates"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row align-items-stretch">{this.props.certificates.map(certificate => this.renderCertificate(certificate))}</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
visibilityCourseCertificates, editMode, saveState, intl,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<SwitchContent
|
||||
className="mb-4"
|
||||
expression={editMode}
|
||||
cases={{
|
||||
editing: (
|
||||
<div role="dialog" aria-labelledby="course-certificates-label">
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<EditableItemHeader
|
||||
headingId="course-certificates-label"
|
||||
content={intl.formatMessage(messages['profile.certificates.my.certificates'])}
|
||||
/>
|
||||
<FormControls
|
||||
visibilityId="visibilityCourseCertificates"
|
||||
saveState={saveState}
|
||||
visibility={visibilityCourseCertificates}
|
||||
cancelHandler={this.handleClose}
|
||||
changeHandler={this.handleChange}
|
||||
/>
|
||||
{this.renderCertificates()}
|
||||
</form>
|
||||
</div>
|
||||
),
|
||||
editable: (
|
||||
<>
|
||||
<EditableItemHeader
|
||||
content={intl.formatMessage(messages['profile.certificates.my.certificates'])}
|
||||
showEditButton
|
||||
onClickEdit={this.handleOpen}
|
||||
showVisibility={visibilityCourseCertificates !== null}
|
||||
visibility={visibilityCourseCertificates}
|
||||
/>
|
||||
{this.renderCertificates()}
|
||||
</>
|
||||
),
|
||||
empty: (
|
||||
<>
|
||||
<EditableItemHeader
|
||||
content={intl.formatMessage(messages['profile.certificates.my.certificates'])}
|
||||
showEditButton
|
||||
onClickEdit={this.handleOpen}
|
||||
showVisibility={visibilityCourseCertificates !== null}
|
||||
visibility={visibilityCourseCertificates}
|
||||
/>
|
||||
{this.renderCertificates()}
|
||||
</>
|
||||
),
|
||||
static: (
|
||||
<>
|
||||
<EditableItemHeader content={intl.formatMessage(messages['profile.certificates.my.certificates'])} />
|
||||
{this.renderCertificates()}
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Certificates.propTypes = {
|
||||
// It'd be nice to just set this as a defaultProps...
|
||||
// except the class that comes out on the other side of react-redux's
|
||||
// connect() method won't have it anymore. Static properties won't survive
|
||||
// through the higher order function.
|
||||
formId: PropTypes.string.isRequired,
|
||||
|
||||
// From Selector
|
||||
certificates: PropTypes.arrayOf(PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
})),
|
||||
visibilityCourseCertificates: PropTypes.oneOf(['private', 'all_users']),
|
||||
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
|
||||
saveState: PropTypes.string,
|
||||
|
||||
// Actions
|
||||
changeHandler: PropTypes.func.isRequired,
|
||||
submitHandler: PropTypes.func.isRequired,
|
||||
closeHandler: PropTypes.func.isRequired,
|
||||
openHandler: PropTypes.func.isRequired,
|
||||
|
||||
// i18n
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
Certificates.defaultProps = {
|
||||
editMode: 'static',
|
||||
saveState: null,
|
||||
visibilityCourseCertificates: 'private',
|
||||
certificates: null,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
certificatesSelector,
|
||||
{},
|
||||
)(injectIntl(Certificates));
|
||||
@@ -1,31 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'profile.certificates.my.certificates': {
|
||||
id: 'profile.certificates.my.certificates',
|
||||
defaultMessage: 'My Certificates',
|
||||
description: 'A section of a user profile',
|
||||
},
|
||||
'profile.certificates.view.certificate': {
|
||||
id: 'profile.certificates.view.certificate',
|
||||
defaultMessage: 'View Certificate',
|
||||
description: 'A call to action to view a certificate',
|
||||
},
|
||||
'profile.certificates.types.verified': {
|
||||
id: 'profile.certificates.types.verified',
|
||||
defaultMessage: 'Verified Certificate',
|
||||
description: 'A type of certificate a user may have earned',
|
||||
},
|
||||
'profile.certificates.types.professional': {
|
||||
id: 'profile.certificates.types.professional',
|
||||
defaultMessage: 'Professional Certificate',
|
||||
description: 'A type of certificate a user may have earned',
|
||||
},
|
||||
'profile.certificates.types.unknown': {
|
||||
id: 'profile.certificates.types.unknown',
|
||||
defaultMessage: 'Certificate',
|
||||
description: 'The string to display when a certificate is of an unknown type',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,160 +1,137 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
import messages from './Country.messages';
|
||||
|
||||
// Components
|
||||
import FormControls from './elements/FormControls';
|
||||
import EditableItemHeader from './elements/EditableItemHeader';
|
||||
import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Selectors
|
||||
import { countrySelector } from '../data/selectors';
|
||||
import {
|
||||
useCloseOpenHandler,
|
||||
useHandleChange,
|
||||
useHandleSubmit,
|
||||
useIsVisibilityEnabled,
|
||||
} from '../data/hooks';
|
||||
|
||||
class Country extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const Country = ({
|
||||
formId,
|
||||
country,
|
||||
visibilityCountry,
|
||||
editMode,
|
||||
saveState,
|
||||
error,
|
||||
translatedCountries,
|
||||
countriesCodesList,
|
||||
countryMessages,
|
||||
changeHandler,
|
||||
submitHandler,
|
||||
closeHandler,
|
||||
openHandler,
|
||||
}) => {
|
||||
const isVisibilityEnabled = useIsVisibilityEnabled();
|
||||
const intl = useIntl();
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.handleOpen = this.handleOpen.bind(this);
|
||||
this.isDisabledCountry = this.isDisabledCountry.bind(this);
|
||||
}
|
||||
const handleChange = useHandleChange(changeHandler);
|
||||
const handleSubmit = useHandleSubmit(submitHandler, formId);
|
||||
const handleOpen = useCloseOpenHandler(openHandler, formId);
|
||||
const handleClose = useCloseOpenHandler(closeHandler, formId);
|
||||
|
||||
handleChange(e) {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
} = e.target;
|
||||
this.props.changeHandler(name, value);
|
||||
}
|
||||
const isDisabledCountry = (countryCode) => countriesCodesList.length > 0
|
||||
&& !countriesCodesList.find(code => code === countryCode);
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.props.submitHandler(this.props.formId);
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
this.props.closeHandler(this.props.formId);
|
||||
}
|
||||
|
||||
handleOpen() {
|
||||
this.props.openHandler(this.props.formId);
|
||||
}
|
||||
|
||||
isDisabledCountry = (country) => {
|
||||
const { countriesCodesList } = this.props;
|
||||
|
||||
return countriesCodesList.length > 0 && !countriesCodesList.find(code => code === country);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
formId,
|
||||
country,
|
||||
visibilityCountry,
|
||||
editMode,
|
||||
saveState,
|
||||
error,
|
||||
intl,
|
||||
translatedCountries,
|
||||
countryMessages,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<SwitchContent
|
||||
className="mb-5"
|
||||
expression={editMode}
|
||||
cases={{
|
||||
editing: (
|
||||
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<Form.Group
|
||||
controlId={formId}
|
||||
isInvalid={error !== null}
|
||||
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}
|
||||
>
|
||||
<label className="edit-section-header" htmlFor={formId}>
|
||||
{intl.formatMessage(messages['profile.country.label'])}
|
||||
</label>
|
||||
<select
|
||||
data-hj-suppress
|
||||
className="form-control"
|
||||
type="select"
|
||||
id={formId}
|
||||
name={formId}
|
||||
value={country}
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
<option value=""> </option>
|
||||
{translatedCountries.map(({ code, name }) => (
|
||||
<option key={code} value={code} disabled={this.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={this.handleClose}
|
||||
changeHandler={this.handleChange}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
),
|
||||
editable: (
|
||||
<>
|
||||
<EditableItemHeader
|
||||
content={intl.formatMessage(messages['profile.country.label'])}
|
||||
showEditButton
|
||||
onClickEdit={this.handleOpen}
|
||||
showVisibility={visibilityCountry !== null}
|
||||
<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}
|
||||
/>
|
||||
<p data-hj-suppress className="h5">{countryMessages[country]}</p>
|
||||
</>
|
||||
),
|
||||
empty: (
|
||||
<>
|
||||
<EditableItemHeader
|
||||
content={intl.formatMessage(messages['profile.country.label'])}
|
||||
/>
|
||||
<EmptyContent onClick={this.handleOpen}>
|
||||
{intl.formatMessage(messages['profile.country.empty'])}
|
||||
</EmptyContent>
|
||||
</>
|
||||
),
|
||||
static: (
|
||||
<>
|
||||
<EditableItemHeader
|
||||
content={intl.formatMessage(messages['profile.country.label'])}
|
||||
/>
|
||||
<p data-hj-suppress className="h5">{countryMessages[country]}</p>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
</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 = {
|
||||
// It'd be nice to just set this as a defaultProps...
|
||||
// except the class that comes out on the other side of react-redux's
|
||||
// connect() method won't have it anymore. Static properties won't survive
|
||||
// through the higher order function.
|
||||
formId: PropTypes.string.isRequired,
|
||||
|
||||
// From Selector
|
||||
country: PropTypes.string,
|
||||
visibilityCountry: PropTypes.oneOf(['private', 'all_users']),
|
||||
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
|
||||
@@ -166,15 +143,10 @@ Country.propTypes = {
|
||||
})).isRequired,
|
||||
countriesCodesList: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
countryMessages: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
|
||||
// Actions
|
||||
changeHandler: PropTypes.func.isRequired,
|
||||
submitHandler: PropTypes.func.isRequired,
|
||||
closeHandler: PropTypes.func.isRequired,
|
||||
openHandler: PropTypes.func.isRequired,
|
||||
|
||||
// i18n
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
Country.defaultProps = {
|
||||
@@ -188,4 +160,4 @@ Country.defaultProps = {
|
||||
export default connect(
|
||||
countrySelector,
|
||||
{},
|
||||
)(injectIntl(Country));
|
||||
)(Country);
|
||||
|
||||
@@ -3,12 +3,12 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
const messages = defineMessages({
|
||||
'profile.country.label': {
|
||||
id: 'profile.country.label',
|
||||
defaultMessage: 'Location',
|
||||
defaultMessage: 'Country',
|
||||
description: 'The label for a country in a user profile.',
|
||||
},
|
||||
'profile.country.empty': {
|
||||
id: 'profile.country.empty',
|
||||
defaultMessage: 'Add location',
|
||||
defaultMessage: 'Add country',
|
||||
description: 'The affordance to add country location to a user’s profile.',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,180 +1,160 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import get from 'lodash.get';
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
import messages from './Education.messages';
|
||||
|
||||
// Components
|
||||
import FormControls from './elements/FormControls';
|
||||
import EditableItemHeader from './elements/EditableItemHeader';
|
||||
import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Constants
|
||||
import { EDUCATION_LEVELS } from '../data/constants';
|
||||
|
||||
// Selectors
|
||||
import { editableFormSelector } from '../data/selectors';
|
||||
import {
|
||||
useCloseOpenHandler,
|
||||
useHandleChange,
|
||||
useHandleSubmit,
|
||||
useIsVisibilityEnabled,
|
||||
} from '../data/hooks';
|
||||
|
||||
class Education extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const Education = ({
|
||||
formId,
|
||||
levelOfEducation,
|
||||
visibilityLevelOfEducation,
|
||||
editMode,
|
||||
saveState,
|
||||
error,
|
||||
changeHandler,
|
||||
submitHandler,
|
||||
closeHandler,
|
||||
openHandler,
|
||||
}) => {
|
||||
const isVisibilityEnabled = useIsVisibilityEnabled();
|
||||
const intl = useIntl();
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.handleOpen = this.handleOpen.bind(this);
|
||||
}
|
||||
const handleChange = useHandleChange(changeHandler);
|
||||
const handleSubmit = useHandleSubmit(submitHandler, formId);
|
||||
const handleOpen = useCloseOpenHandler(openHandler, formId);
|
||||
const handleClose = useCloseOpenHandler(closeHandler, formId);
|
||||
|
||||
handleChange(e) {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
} = e.target;
|
||||
this.props.changeHandler(name, value);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.props.submitHandler(this.props.formId);
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
this.props.closeHandler(this.props.formId);
|
||||
}
|
||||
|
||||
handleOpen() {
|
||||
this.props.openHandler(this.props.formId);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
formId, levelOfEducation, visibilityLevelOfEducation, editMode, saveState, error, intl,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<SwitchContent
|
||||
className="mb-5"
|
||||
expression={editMode}
|
||||
cases={{
|
||||
editing: (
|
||||
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<Form.Group
|
||||
controlId={formId}
|
||||
isInvalid={error !== null}
|
||||
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}
|
||||
>
|
||||
<label className="edit-section-header" htmlFor={formId}>
|
||||
{intl.formatMessage(messages['profile.education.education'])}
|
||||
</label>
|
||||
<select
|
||||
data-hj-suppress
|
||||
className="form-control"
|
||||
id={formId}
|
||||
name={formId}
|
||||
value={levelOfEducation}
|
||||
onChange={this.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={this.handleClose}
|
||||
changeHandler={this.handleChange}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
),
|
||||
editable: (
|
||||
<>
|
||||
<EditableItemHeader
|
||||
content={intl.formatMessage(messages['profile.education.education'])}
|
||||
showEditButton
|
||||
onClickEdit={this.handleOpen}
|
||||
showVisibility={visibilityLevelOfEducation !== null}
|
||||
<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}
|
||||
/>
|
||||
<p data-hj-suppress className="h5">
|
||||
{intl.formatMessage(get(
|
||||
messages,
|
||||
`profile.education.levels.${levelOfEducation}`,
|
||||
messages['profile.education.levels.o'],
|
||||
))}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
empty: (
|
||||
<>
|
||||
<EditableItemHeader content={intl.formatMessage(messages['profile.education.education'])} />
|
||||
<EmptyContent onClick={this.handleOpen}>
|
||||
<FormattedMessage
|
||||
id="profile.education.empty"
|
||||
defaultMessage="Add education"
|
||||
description="instructions when the user doesn't have their level of education set"
|
||||
/>
|
||||
</EmptyContent>
|
||||
</>
|
||||
),
|
||||
static: (
|
||||
<>
|
||||
<EditableItemHeader content={intl.formatMessage(messages['profile.education.education'])} />
|
||||
<p data-hj-suppress className="h5">
|
||||
{intl.formatMessage(get(
|
||||
messages,
|
||||
`profile.education.levels.${levelOfEducation}`,
|
||||
messages['profile.education.levels.o'],
|
||||
))}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
</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 = {
|
||||
// It'd be nice to just set this as a defaultProps...
|
||||
// except the class that comes out on the other side of react-redux's
|
||||
// connect() method won't have it anymore. Static properties won't survive
|
||||
// through the higher order function.
|
||||
formId: PropTypes.string.isRequired,
|
||||
|
||||
// From Selector
|
||||
levelOfEducation: PropTypes.string,
|
||||
visibilityLevelOfEducation: PropTypes.oneOf(['private', 'all_users']),
|
||||
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
|
||||
saveState: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
|
||||
// Actions
|
||||
changeHandler: PropTypes.func.isRequired,
|
||||
submitHandler: PropTypes.func.isRequired,
|
||||
closeHandler: PropTypes.func.isRequired,
|
||||
openHandler: PropTypes.func.isRequired,
|
||||
|
||||
// i18n
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
Education.defaultProps = {
|
||||
@@ -188,4 +168,4 @@ Education.defaultProps = {
|
||||
export default connect(
|
||||
editableFormSelector,
|
||||
{},
|
||||
)(injectIntl(Education));
|
||||
)(Education);
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import get from 'lodash.get';
|
||||
|
||||
// Mock Data
|
||||
import mockData from '../data/mock_data';
|
||||
|
||||
import messages from './LearningGoal.messages';
|
||||
|
||||
// Components
|
||||
import EditableItemHeader from './elements/EditableItemHeader';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Selectors
|
||||
import { editableFormSelector } from '../data/selectors';
|
||||
|
||||
const LearningGoal = (props) => {
|
||||
let { learningGoal, editMode, visibilityLearningGoal } = props;
|
||||
const { intl } = props;
|
||||
|
||||
if (!learningGoal) {
|
||||
learningGoal = mockData.learningGoal;
|
||||
}
|
||||
|
||||
if (!editMode || editMode === 'empty') { // editMode defaults to 'empty', not sure why yet
|
||||
editMode = mockData.editMode;
|
||||
}
|
||||
|
||||
if (!visibilityLearningGoal) {
|
||||
visibilityLearningGoal = mockData.visibilityLearningGoal;
|
||||
}
|
||||
|
||||
return (
|
||||
<SwitchContent
|
||||
className="mb-5"
|
||||
expression={editMode}
|
||||
cases={{
|
||||
editable: (
|
||||
<>
|
||||
<EditableItemHeader
|
||||
content={intl.formatMessage(messages['profile.learningGoal.learningGoal'])}
|
||||
showVisibility={visibilityLearningGoal !== null}
|
||||
visibility={visibilityLearningGoal}
|
||||
/>
|
||||
<p data-hj-suppress className="lead">
|
||||
{intl.formatMessage(get(
|
||||
messages,
|
||||
`profile.learningGoal.options.${learningGoal}`,
|
||||
messages['profile.learningGoal.options.something_else'],
|
||||
))}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
static: (
|
||||
<>
|
||||
<EditableItemHeader content={intl.formatMessage(messages['profile.learningGoal.learningGoal'])} />
|
||||
<p data-hj-suppress className="lead">
|
||||
{intl.formatMessage(get(
|
||||
messages,
|
||||
`profile.learningGoal.options.${learningGoal}`,
|
||||
messages['profile.learningGoal.options.something_else'],
|
||||
))}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
LearningGoal.propTypes = {
|
||||
// From Selector
|
||||
learningGoal: PropTypes.oneOf(['advance_career', 'start_career', 'learn_something_new', 'something_else']),
|
||||
visibilityLearningGoal: PropTypes.oneOf(['private', 'all_users']),
|
||||
editMode: PropTypes.oneOf(['editable', 'static']),
|
||||
|
||||
// i18n
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
LearningGoal.defaultProps = {
|
||||
editMode: 'static',
|
||||
learningGoal: null,
|
||||
visibilityLearningGoal: 'private',
|
||||
};
|
||||
|
||||
export default connect(
|
||||
editableFormSelector,
|
||||
{},
|
||||
)(injectIntl(LearningGoal));
|
||||
@@ -1,31 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'profile.learningGoal.learningGoal': {
|
||||
id: 'profile.learningGoal.learningGoal',
|
||||
defaultMessage: 'Learning Goal',
|
||||
description: 'A section of a user profile that displays their current learning goal.',
|
||||
},
|
||||
'profile.learningGoal.options.start_career': {
|
||||
id: 'profile.learningGoal.options.start_career',
|
||||
defaultMessage: 'I want to start my career',
|
||||
description: 'Selected by user if their goal is to start their career.',
|
||||
},
|
||||
'profile.learningGoal.options.advance_career': {
|
||||
id: 'profile.learningGoal.options.advance_career',
|
||||
defaultMessage: 'I want to advance my career',
|
||||
description: 'Selected by user if their goal is to advance their career.',
|
||||
},
|
||||
'profile.learningGoal.options.learn_something_new': {
|
||||
id: 'profile.learningGoal.options.learn_something_new',
|
||||
defaultMessage: 'I want to learn something new',
|
||||
description: 'Selected by user if their goal is to learn something new.',
|
||||
},
|
||||
'profile.learningGoal.options.something_else': {
|
||||
id: 'profile.learningGoal.options.something_else',
|
||||
defaultMessage: 'Something else',
|
||||
description: 'Selected by user if their goal is not described by the other choices.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user