diff --git a/src/account-settings/AccountSettingsPage.jsx b/src/account-settings/AccountSettingsPage.jsx index 1ec196a..98e7789 100644 --- a/src/account-settings/AccountSettingsPage.jsx +++ b/src/account-settings/AccountSettingsPage.jsx @@ -1,17 +1,18 @@ import { AppContext } from '@edx/frontend-platform/react'; -import { getConfig, getQueryParameters } from '@edx/frontend-platform'; -import React from 'react'; +import { getConfig } from '@edx/frontend-platform'; +import { + useEffect, useContext, useMemo, createRef, useState, +} from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import memoize from 'memoize-one'; import findIndex from 'lodash.findindex'; import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics'; import { - injectIntl, - intlShape, FormattedMessage, getCountryList, getLanguageList, + useIntl, } from '@edx/frontend-platform/i18n'; import { Container, Hyperlink, Icon, Alert, @@ -54,117 +55,87 @@ import { fetchNotificationPreferences } from '../notification-preferences/data/t import NotificationSettings from '../notification-preferences/NotificationSettings'; import { withLocation, withNavigate } from './hoc'; -class AccountSettingsPage extends React.Component { - constructor(props, context) { - super(props, context); +const AccountSettingsPage = ({ + loading = false, + loaded = false, + loadingError = null, + nameChangeModal = {} || false, + navigate, + countriesCodesList = [], + profileDataManager = null, + committedValues = { + useVerifiedNameForCerts: false, + verified_name: null, + country: '', + }, + ...props +}) => { + const intl = useIntl(); + const appContext = useContext(AppContext); + const [duplicateTpaProvider, setDuplicateTpaProvider] = useState(null); - const duplicateTpaProvider = getQueryParameters().duplicate_provider; - this.state = { - duplicateTpaProvider, - }; - - this.navLinkRefs = { - '#basic-information': React.createRef(), - '#profile-information': React.createRef(), - '#social-media': React.createRef(), - '#notifications': React.createRef(), - '#site-preferences': React.createRef(), - '#linked-accounts': React.createRef(), - '#delete-account': React.createRef(), - }; - } - - componentDidMount() { - this.props.fetchNotificationPreferences(); - this.props.fetchSettings(); - this.props.fetchSiteLanguages(this.props.navigate); + useEffect(() => { + const searchParams = new URLSearchParams(window.location.search); + const initialDuplicateTpaProvider = searchParams.get('duplicate_provider'); + if (initialDuplicateTpaProvider) { + setDuplicateTpaProvider(initialDuplicateTpaProvider); + } + props.fetchNotificationPreferences(); + props.fetchSettings(); + props.fetchSiteLanguages(navigate); sendTrackingLogEvent('edx.user.settings.viewed', { page: 'account', visibility: null, - user_id: this.context.authenticatedUser.userId, + user_id: appContext.authenticatedUser.userId, }); - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - componentDidUpdate(prevProps) { - if (prevProps.loading && !prevProps.loaded && this.props.loaded) { + const navLinkRefs = useMemo(() => ({ + '#basic-information': createRef(), + '#profile-information': createRef(), + '#social-media': createRef(), + '#notifications': createRef(), + '#site-preferences': createRef(), + '#linked-accounts': createRef(), + '#delete-account': createRef(), + }), []); + + useEffect(() => { + if (loading && !loaded && loaded) { const locationHash = global.location.hash; // Check for the locationHash in the URL and then scroll to it if it is in the // NavLinks list if (typeof locationHash !== 'string') { return; } - if (Object.keys(this.navLinkRefs).includes(locationHash) && this.navLinkRefs[locationHash].current) { - window.scrollTo(0, this.navLinkRefs[locationHash].current.offsetTop); + if (Object.keys(navLinkRefs).includes(locationHash) && navLinkRefs[locationHash].current) { + window.scrollTo(0, navLinkRefs[locationHash].current.offsetTop); } } - } + }, [loading, loaded, navLinkRefs]); // NOTE: We need 'locale' for the memoization in getLocalizedTimeZoneOptions. Don't remove it! // eslint-disable-next-line no-unused-vars - getLocalizedTimeZoneOptions = memoize((timeZoneOptions, countryTimeZoneOptions, locale) => { + const getLocalizedTimeZoneOptions = memoize((timeZoneOptions, countryTimeZoneOptions, locale) => { const concatTimeZoneOptions = [{ - label: this.props.intl.formatMessage(messages['account.settings.field.time.zone.default']), + label: intl.formatMessage(messages['account.settings.field.time.zone.default']), value: '', }]; if (countryTimeZoneOptions.length) { concatTimeZoneOptions.push({ - label: this.props.intl.formatMessage(messages['account.settings.field.time.zone.country']), + label: intl.formatMessage(messages['account.settings.field.time.zone.country']), group: countryTimeZoneOptions, }); } concatTimeZoneOptions.push({ - label: this.props.intl.formatMessage(messages['account.settings.field.time.zone.all']), + label: intl.formatMessage(messages['account.settings.field.time.zone.all']), group: timeZoneOptions, }); return concatTimeZoneOptions; }); - getLocalizedOptions = memoize((locale, country) => ({ - countryOptions: [{ - value: '', - label: this.props.intl.formatMessage(messages['account.settings.field.country.options.empty']), - }].concat( - this.removeDisabledCountries( - getCountryList(locale).map(({ code, name }) => ({ - value: code, - label: name, - disabled: this.isDisabledCountry(code), - })), - ), - ), - stateOptions: [{ - value: '', - label: this.props.intl.formatMessage(messages['account.settings.field.state.options.empty']), - }].concat(getStatesList(country)), - languageProficiencyOptions: [{ - value: '', - label: this.props.intl.formatMessage(messages['account.settings.field.language_proficiencies.options.empty']), - }].concat(getLanguageList(locale).map(({ code, name }) => ({ value: code, label: name }))), - yearOfBirthOptions: [{ - value: '', - label: this.props.intl.formatMessage(messages['account.settings.field.year_of_birth.options.empty']), - }].concat(YEAR_OF_BIRTH_OPTIONS), - educationLevelOptions: EDUCATION_LEVELS.map(key => ({ - value: key, - label: this.props.intl.formatMessage(messages[`account.settings.field.education.levels.${key || 'empty'}`]), - })), - genderOptions: GENDER_OPTIONS.map(key => ({ - value: key, - label: this.props.intl.formatMessage(messages[`account.settings.field.gender.options.${key || 'empty'}`]), - })), - workExperienceOptions: WORK_EXPERIENCE_OPTIONS.map(key => ({ - value: key, - label: key === '' ? this.props.intl.formatMessage(messages['account.settings.field.work.experience.options.empty']) : key, - })), - })); - - canDeleteAccount = () => { - const { committedValues } = this.props; - return !getConfig().COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED.includes(committedValues.country); - }; - - removeDisabledCountries = (countryList) => { - const { countriesCodesList, committedValues } = this.props; + const removeDisabledCountries = (countryList) => { const committedCountry = committedValues?.country; if (!countriesCodesList.length) { @@ -173,16 +144,59 @@ class AccountSettingsPage extends React.Component { return countryList.filter(({ value }) => value === committedCountry || countriesCodesList.find(x => x === value)); }; - handleEditableFieldChange = (name, value) => { - this.props.updateDraft(name, value); + const isDisabledCountry = (country) => countriesCodesList.length > 0 && !countriesCodesList.find(x => x === country); + + const getLocalizedOptions = memoize((locale, country) => ({ + countryOptions: [{ + value: '', + label: intl.formatMessage(messages['account.settings.field.country.options.empty']), + }].concat( + removeDisabledCountries( + getCountryList(locale).map(({ code, name }) => ({ + value: code, + label: name, + disabled: isDisabledCountry(code), + })), + ), + ), + stateOptions: [{ + value: '', + label: intl.formatMessage(messages['account.settings.field.state.options.empty']), + }].concat(getStatesList(country)), + languageProficiencyOptions: [{ + value: '', + label: intl.formatMessage(messages['account.settings.field.language_proficiencies.options.empty']), + }].concat(getLanguageList(locale).map(({ code, name }) => ({ value: code, label: name }))), + yearOfBirthOptions: [{ + value: '', + label: intl.formatMessage(messages['account.settings.field.year_of_birth.options.empty']), + }].concat(YEAR_OF_BIRTH_OPTIONS), + educationLevelOptions: EDUCATION_LEVELS.map(key => ({ + value: key, + label: intl.formatMessage(messages[`account.settings.field.education.levels.${key || 'empty'}`]), + })), + genderOptions: GENDER_OPTIONS.map(key => ({ + value: key, + label: intl.formatMessage(messages[`account.settings.field.gender.options.${key || 'empty'}`]), + })), + workExperienceOptions: WORK_EXPERIENCE_OPTIONS.map(key => ({ + value: key, + label: key === '' ? intl.formatMessage(messages['account.settings.field.work.experience.options.empty']) : key, + })), + })); + + const canDeleteAccount = () => !getConfig().COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED.includes(committedValues.country); + + const handleEditableFieldChange = (name, value) => { + updateDraft(name, value); }; - handleSubmit = (formId, values) => { - if (formId === FIELD_LABELS.COUNTRY && this.isDisabledCountry(values)) { + const handleSubmit = (formId, values) => { + if (formId === FIELD_LABELS.COUNTRY && isDisabledCountry(values)) { return; } - const { formValues } = this.props; + const { formValues } = props; let extendedProfileObject = {}; if ('extended_profile' in formValues && formValues.extended_profile.some((field) => field.field_name === formId)) { @@ -192,55 +206,45 @@ class AccountSettingsPage extends React.Component { : field)), }; } - this.props.saveSettings(formId, values, extendedProfileObject); + saveSettings(formId, values, extendedProfileObject); }; - handleSubmitProfileName = (formId, values) => { - if (Object.keys(this.props.drafts).includes('useVerifiedNameForCerts')) { - this.props.saveMultipleSettings([ + const handleSubmitProfileName = (formId, values) => { + if (Object.keys(props.drafts).includes('useVerifiedNameForCerts')) { + saveMultipleSettings([ { formId, commitValues: values, }, { formId: 'useVerifiedNameForCerts', - commitValues: this.props.formValues.useVerifiedNameForCerts, + commitValues: props.formValues.useVerifiedNameForCerts, }, ], formId); } else { - this.props.saveSettings(formId, values); + saveSettings(formId, values); } }; - handleSubmitVerifiedName = (formId, values) => { - if (Object.keys(this.props.drafts).includes('useVerifiedNameForCerts')) { - this.props.saveSettings('useVerifiedNameForCerts', this.props.formValues.useVerifiedNameForCerts); + const handleSubmitVerifiedName = (formId, values) => { + if (Object.keys(props.drafts).includes('useVerifiedNameForCerts')) { + saveSettings('useVerifiedNameForCerts', props.formValues.useVerifiedNameForCerts); } - if (values !== this.props.committedValues?.verified_name) { - this.props.beginNameChange(formId); + if (values !== props.committedValues?.verified_name) { + beginNameChange(formId); } else { - this.props.saveSettings(formId, values); + saveSettings(formId, values); } }; - isDisabledCountry = (country) => { - const { countriesCodesList } = this.props; + const isEditable = (fieldName) => !props.staticFields.includes(fieldName); - return countriesCodesList.length > 0 && !countriesCodesList.find(x => x === country); - }; + // Enterprise customer profiles are managed by their organizations. We determine whether + // a profile is managed or not by the presence of the profileDataManager prop. + const isManagedProfile = () => Boolean(profileDataManager); - isEditable(fieldName) { - return !this.props.staticFields.includes(fieldName); - } - - isManagedProfile() { - // Enterprise customer profiles are managed by their organizations. We determine whether - // a profile is managed or not by the presence of the profileDataManager prop. - return Boolean(this.props.profileDataManager); - } - - renderDuplicateTpaProviderMessage() { - if (!this.state.duplicateTpaProvider) { + const renderDuplicateTpaProviderMessage = () => { + if (!duplicateTpaProvider) { return null; } @@ -248,7 +252,7 @@ class AccountSettingsPage extends React.Component { // way of telling us that the provider account the user tried to link is already linked // to another user account on the platform. We use this to display a message to that effect, // and remove the parameter from the URL. - this.props.navigate(this.props.location, { replace: true }); + navigate(props.location, { replace: true }); return (
- {this.props.intl.formatMessage(messages['account.settings.field.name.verified.submitted.message'])}{' '} + {intl.formatMessage(messages['account.settings.field.name.verified.submitted.message'])}{' '} { willCertNameChange - && this.props.intl.formatMessage(messages['account.settings.field.name.verified.submitted.message.certificate']) + && intl.formatMessage(messages['account.settings.field.name.verified.submitted.message.certificate']) }
{this.props.intl.formatMessage(messages['account.settings.section.account.information.description'])}
- {this.renderManagedProfileMessage()} +{intl.formatMessage(messages['account.settings.section.account.information.description'])}
+ {renderManagedProfileMessage()} - {this.renderNameChangeModal()} + {renderNameChangeModal()}- {this.props.intl.formatMessage( + {intl.formatMessage( messages['account.settings.section.linked.accounts.description'], { siteName: getConfig().SITE_NAME }, )} @@ -816,68 +820,53 @@ class AccountSettingsPage extends React.Component {
- {this.props.intl.formatMessage(messages['account.settings.section.social.media'])} + {intl.formatMessage(messages['account.settings.section.social.media'])}
- {this.props.intl.formatMessage( + {intl.formatMessage( messages['account.settings.section.social.media.description'], { siteName: getConfig().SITE_NAME }, )} @@ -747,67 +751,67 @@ class AccountSettingsPage extends React.Component {