Compare commits
15 Commits
frontend-b
...
sundasAcco
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42342c2b18 | ||
|
|
5a5de29dea | ||
|
|
5381719477 | ||
|
|
235dd9c462 | ||
|
|
301797aca3 | ||
|
|
27c686fbe3 | ||
|
|
45bcfddab6 | ||
|
|
45b0734e83 | ||
|
|
8db7c54119 | ||
|
|
f04fddd8c0 | ||
|
|
84de80eb7f | ||
|
|
7618004054 | ||
|
|
3b2cf2717e | ||
|
|
dbb374860d | ||
|
|
aeff548b82 |
3
.env
3
.env
@@ -15,7 +15,7 @@ LOGO_WHITE_URL=''
|
||||
SHOW_EMAIL_CHANNEL=''
|
||||
LOGOUT_URL=''
|
||||
MARKETING_SITE_BASE_URL=''
|
||||
NODE_ENV='production'
|
||||
NODE_ENV=''
|
||||
ORDER_HISTORY_URL=''
|
||||
PUBLISHER_BASE_URL=''
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
||||
@@ -33,4 +33,3 @@ MFE_CONFIG_API_URL=
|
||||
PASSWORD_RESET_SUPPORT_LINK=''
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account'
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'
|
||||
|
||||
@@ -34,4 +34,3 @@ MFE_CONFIG_API_URL=
|
||||
PASSWORD_RESET_SUPPORT_LINK='mailto:support@example.com'
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account'
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'
|
||||
|
||||
@@ -31,4 +31,3 @@ APP_ID=
|
||||
MFE_CONFIG_API_URL=
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account'
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'
|
||||
|
||||
@@ -3,4 +3,3 @@ dist/
|
||||
node_modules/
|
||||
__mocks__/
|
||||
__snapshots__/
|
||||
src/i18n/messages/
|
||||
|
||||
18
Makefile
18
Makefile
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
@@ -17,11 +19,6 @@ test.npm.%: validate-no-uncommitted-package-lock-changes
|
||||
npm run $(*)
|
||||
|
||||
.PHONY: requirements
|
||||
|
||||
precommit:
|
||||
npm run lint
|
||||
npm audit
|
||||
|
||||
requirements: ## install ci requirements
|
||||
npm ci
|
||||
|
||||
@@ -41,17 +38,6 @@ detect_changed_source_translations:
|
||||
# Checking for changed translations...
|
||||
git diff --exit-code $(i18n)
|
||||
|
||||
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||
push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
|
||||
40
README.rst
40
README.rst
@@ -25,13 +25,40 @@ Getting Started
|
||||
Prerequisites
|
||||
=============
|
||||
|
||||
`Tutor`_ is currently recommended as a development environment for your
|
||||
new MFE. Please refer
|
||||
The `devstack`_ is currently recommended as a development environment for your
|
||||
new MFE. If you start it with ``make dev.up.lms`` that should give you
|
||||
everything you need as a companion to this frontend.
|
||||
|
||||
Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer
|
||||
to the `relevant tutor-mfe documentation`_ to get started using it.
|
||||
|
||||
.. _Devstack: https://github.com/openedx/devstack
|
||||
|
||||
.. _Tutor: https://github.com/overhangio/tutor
|
||||
|
||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
|
||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
|
||||
|
||||
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
|
||||
|
||||
2. Start up Devstack, if it's not already started.
|
||||
|
||||
3. Log in to Devstack (http://localhost:18000/login )
|
||||
|
||||
4. Within this project, install requirements and start the development server:
|
||||
|
||||
.. code-block::
|
||||
|
||||
npm install
|
||||
npm start # The server will run on port 1997
|
||||
|
||||
5. Once the dev server is up, visit http://localhost:1997 to access the MFE
|
||||
|
||||
.. image:: ./docs/images/localhost_preview.png
|
||||
|
||||
Plugins
|
||||
=======
|
||||
@@ -42,7 +69,7 @@ The parts of this MFE that can be customized in that manner are documented `here
|
||||
Environment Variables/Setup Notes
|
||||
=================================
|
||||
|
||||
This MFE is configured via the ``frontend-platform`` configuration module. For more information on MFE configuration see the `Configuration documentation`_.
|
||||
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
|
||||
The account settings micro-frontend also supports the following additional variable:
|
||||
|
||||
@@ -75,9 +102,8 @@ Example build syntax with a single environment variable:
|
||||
|
||||
NODE_ENV=development ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build
|
||||
|
||||
For more information see the document: `Configuration documentation`_
|
||||
|
||||
.. _Configuration documentation: https://openedx.github.io/frontend-platform/module-Config.html
|
||||
For more information see the document: `Micro-frontend applications in Open
|
||||
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
|
||||
Cloning and Startup
|
||||
===================
|
||||
|
||||
@@ -12,8 +12,7 @@ metadata:
|
||||
icon: "Web"
|
||||
annotations:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
owner: group:2u-infinity
|
||||
owner: group:edx-infinity
|
||||
type: 'website'
|
||||
lifecycle: 'production'
|
||||
|
||||
6
openedx.yaml
Normal file
6
openedx.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0002-bp-repo-metadata.html#specification
|
||||
|
||||
nick: acct
|
||||
oeps: {}
|
||||
openedx-release: {ref: master}
|
||||
7388
package-lock.json
generated
7388
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -29,23 +29,23 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-platform": "^8.3.3",
|
||||
"@edx/openedx-atlas": "^0.7.0",
|
||||
"@edx/frontend-component-header": "^5.6.0",
|
||||
"@edx/frontend-platform": "8.1.2",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/paragon": "^22.16.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.2.2",
|
||||
"@openedx/frontend-slot-footer": "^1.0.2",
|
||||
"@openedx/paragon": "22.9.0",
|
||||
"@tensorflow-models/blazeface": "0.1.0",
|
||||
"@tensorflow/tfjs-converter": "4.22.0",
|
||||
"@tensorflow/tfjs-core": "4.22.0",
|
||||
"bowser": "2.11.0",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.41.0",
|
||||
"core-js": "3.38.1",
|
||||
"font-awesome": "4.7.0",
|
||||
"form-urlencoded": "6.1.5",
|
||||
"formdata-polyfill": "4.0.10",
|
||||
@@ -60,12 +60,12 @@
|
||||
"lodash.pick": "4.4.0",
|
||||
"lodash.pickby": "4.6.0",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"long": "5.3.2",
|
||||
"long": "5.2.3",
|
||||
"memoize-one": "^6.0.0",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.14.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"qs": "6.13.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "^6.25.1",
|
||||
@@ -83,12 +83,12 @@
|
||||
"universal-cookie": "7.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.5.0",
|
||||
"@edx/browserslist-config": "1.2.0",
|
||||
"@edx/reactifex": "2.2.0",
|
||||
"@openedx/frontend-build": "^14.3.3",
|
||||
"@openedx/frontend-build": "14.1.5",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "1.5.5"
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ import {
|
||||
COPPA_COMPLIANCE_YEAR,
|
||||
WORK_EXPERIENCE_OPTIONS,
|
||||
getStatesList,
|
||||
FIELD_LABELS,
|
||||
} from './data/constants';
|
||||
import { fetchSiteLanguages } from './site-language';
|
||||
import { fetchCourseList } from '../notification-preferences/data/thunks';
|
||||
@@ -67,7 +66,6 @@ class AccountSettingsPage extends React.Component {
|
||||
'#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(),
|
||||
@@ -158,19 +156,17 @@ class AccountSettingsPage extends React.Component {
|
||||
})),
|
||||
}));
|
||||
|
||||
canDeleteAccount = () => {
|
||||
const { committedValues } = this.props;
|
||||
return !getConfig().COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED.includes(committedValues.country);
|
||||
};
|
||||
|
||||
removeDisabledCountries = (countryList) => {
|
||||
const { countriesCodesList, committedValues } = this.props;
|
||||
const committedCountry = committedValues?.country;
|
||||
const { disabledCountries, committedValues } = this.props;
|
||||
|
||||
if (!countriesCodesList.length) {
|
||||
if (!disabledCountries.length) {
|
||||
return countryList;
|
||||
}
|
||||
return countryList.filter(({ value }) => value === committedCountry || countriesCodesList.find(x => x === value));
|
||||
|
||||
return countryList.filter(({ value, disabled }) => {
|
||||
const isUserCountry = value === committedValues.country;
|
||||
return !disabled || isUserCountry;
|
||||
});
|
||||
};
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
@@ -178,7 +174,7 @@ class AccountSettingsPage extends React.Component {
|
||||
};
|
||||
|
||||
handleSubmit = (formId, values) => {
|
||||
if (formId === FIELD_LABELS.COUNTRY && this.isDisabledCountry(values)) {
|
||||
if (formId === 'country' && this.isDisabledCountry(values)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -224,9 +220,8 @@ class AccountSettingsPage extends React.Component {
|
||||
};
|
||||
|
||||
isDisabledCountry = (country) => {
|
||||
const { countriesCodesList } = this.props;
|
||||
|
||||
return countriesCodesList.length > 0 && !countriesCodesList.find(x => x === country);
|
||||
const { disabledCountries } = this.props;
|
||||
return disabledCountries.includes(country);
|
||||
};
|
||||
|
||||
isEditable(fieldName) {
|
||||
@@ -733,7 +728,7 @@ class AccountSettingsPage extends React.Component {
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</div>
|
||||
<div className="account-section pt-3 mb-6" id="social-media">
|
||||
<div className="account-section pt-3" id="social-media">
|
||||
<h2 className="section-heading h4 mb-3">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
|
||||
</h2>
|
||||
@@ -769,10 +764,9 @@ class AccountSettingsPage extends React.Component {
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</div>
|
||||
<div className="border border-light-700" />
|
||||
<div className="mt-6" id="notifications" ref={this.navLinkRefs['#notifications']}>
|
||||
<NotificationSettings />
|
||||
</div>
|
||||
<div className="border border-light-700 my-6" />
|
||||
<NotificationSettings />
|
||||
<div className="border border-light-700 my-6" />
|
||||
<div className="account-section mb-5" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
|
||||
<h2 className="section-heading h4 mb-3">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])}
|
||||
@@ -815,15 +809,16 @@ class AccountSettingsPage extends React.Component {
|
||||
<ThirdPartyAuth />
|
||||
</div>
|
||||
|
||||
{getConfig().ENABLE_ACCOUNT_DELETION && (
|
||||
{getConfig().ENABLE_ACCOUNT_DELETION
|
||||
&& (
|
||||
<div className="account-section pt-3 mb-5" id="delete-account" ref={this.navLinkRefs['#delete-account']}>
|
||||
<DeleteAccount
|
||||
isVerifiedAccount={this.props.isActive}
|
||||
hasLinkedTPA={hasLinkedTPA}
|
||||
canDeleteAccount={this.canDeleteAccount()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -888,15 +883,12 @@ AccountSettingsPage.propTypes = {
|
||||
name: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
secondary_email: PropTypes.string,
|
||||
secondary_email_enabled: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||
secondary_email_enabled: PropTypes.bool,
|
||||
year_of_birth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
country: PropTypes.string,
|
||||
level_of_education: PropTypes.string,
|
||||
gender: PropTypes.string,
|
||||
extended_profile: PropTypes.arrayOf(PropTypes.shape({
|
||||
field_name: PropTypes.string,
|
||||
field_value: PropTypes.string,
|
||||
})),
|
||||
extended_profile: PropTypes.string,
|
||||
language_proficiencies: PropTypes.string,
|
||||
pending_name_change: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
@@ -949,12 +941,9 @@ AccountSettingsPage.propTypes = {
|
||||
tpaProviders: PropTypes.arrayOf(PropTypes.shape({
|
||||
connected: PropTypes.bool,
|
||||
})),
|
||||
nameChangeModal: PropTypes.oneOfType([
|
||||
PropTypes.shape({
|
||||
formId: PropTypes.string,
|
||||
}),
|
||||
PropTypes.bool,
|
||||
]),
|
||||
nameChangeModal: PropTypes.shape({
|
||||
formId: PropTypes.string,
|
||||
}),
|
||||
verifiedName: PropTypes.shape({
|
||||
verified_name: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
@@ -974,12 +963,7 @@ AccountSettingsPage.propTypes = {
|
||||
),
|
||||
navigate: PropTypes.func.isRequired,
|
||||
location: PropTypes.string.isRequired,
|
||||
countriesCodesList: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
}),
|
||||
),
|
||||
disabledCountries: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
AccountSettingsPage.defaultProps = {
|
||||
@@ -1002,11 +986,11 @@ AccountSettingsPage.defaultProps = {
|
||||
tpaProviders: [],
|
||||
isActive: true,
|
||||
secondary_email_enabled: false,
|
||||
nameChangeModal: {} || false,
|
||||
nameChangeModal: {},
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: [],
|
||||
countriesCodesList: [],
|
||||
disabledCountries: [],
|
||||
};
|
||||
|
||||
export default withLocation(withNavigate(connect(accountSettingsPageSelector, {
|
||||
|
||||
@@ -19,14 +19,12 @@ const JumpNav = ({
|
||||
'basic-information',
|
||||
'profile-information',
|
||||
'social-media',
|
||||
'notifications',
|
||||
'site-preferences',
|
||||
'linked-accounts',
|
||||
'delete-account',
|
||||
]}
|
||||
className="list-unstyled"
|
||||
currentClassName="font-weight-bold"
|
||||
offset={-64}
|
||||
>
|
||||
<li>
|
||||
<NavHashLink to="#basic-information">
|
||||
@@ -43,11 +41,6 @@ const JumpNav = ({
|
||||
{intl.formatMessage(messages['account.settings.section.social.media'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavHashLink to="#notifications">
|
||||
{intl.formatMessage(messages['notification.preferences.notifications.label'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavHashLink to="#site-preferences">
|
||||
{intl.formatMessage(messages['account.settings.section.site.preferences'])}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
@@ -12,11 +13,8 @@ import {
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Modal creates a portal. Overriding createPortal allows portals to be tested in jest.
|
||||
jest.mock('react-dom', () => ({
|
||||
...jest.requireActual('react-dom'),
|
||||
createPortal: jest.fn(node => node), // Mock portal behavior
|
||||
}));
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
|
||||
import CertificatePreference from '../CertificatePreference'; // eslint-disable-line import/first
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ export const fetchSettingsSuccess = ({
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
verifiedNameHistory,
|
||||
countriesCodesList,
|
||||
}) => ({
|
||||
type: FETCH_SETTINGS.SUCCESS,
|
||||
payload: {
|
||||
@@ -36,7 +35,6 @@ export const fetchSettingsSuccess = ({
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
verifiedNameHistory,
|
||||
countriesCodesList,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -132,10 +132,6 @@ export function getStatesList(country) {
|
||||
return country && COUNTRY_STATES_MAP[country.toUpperCase()];
|
||||
}
|
||||
|
||||
export const FIELD_LABELS = {
|
||||
COUNTRY: 'country',
|
||||
};
|
||||
|
||||
export const DECLINED = 'declined';
|
||||
export const SELF_DESCRIBE = 'self-describe';
|
||||
export const OTHER = 'other';
|
||||
|
||||
@@ -39,7 +39,7 @@ export const defaultState = {
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: {},
|
||||
countriesCodesList: [],
|
||||
disabledCountries: ['RU'],
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action = {}) => {
|
||||
@@ -65,7 +65,6 @@ const reducer = (state = defaultState, action = {}) => {
|
||||
loaded: true,
|
||||
loadingError: null,
|
||||
verifiedNameHistory: action.payload.verifiedNameHistory,
|
||||
countriesCodesList: action.payload.countriesCodesList,
|
||||
};
|
||||
case FETCH_SETTINGS.FAILURE:
|
||||
return {
|
||||
|
||||
@@ -53,7 +53,7 @@ export function* handleFetchSettings() {
|
||||
const { username, userId, roles: userRoles } = getAuthenticatedUser();
|
||||
|
||||
const {
|
||||
thirdPartyAuthProviders, profileDataManager, timeZones, countries, ...values
|
||||
thirdPartyAuthProviders, profileDataManager, timeZones, ...values
|
||||
} = yield call(
|
||||
getSettings,
|
||||
username,
|
||||
@@ -71,7 +71,6 @@ export function* handleFetchSettings() {
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
verifiedNameHistory,
|
||||
countriesCodesList: countries,
|
||||
}));
|
||||
} catch (e) {
|
||||
yield put(fetchSettingsFailure(e.message));
|
||||
|
||||
@@ -88,11 +88,6 @@ const previousSiteLanguageSelector = createSelector(
|
||||
accountSettings => accountSettings.previousSiteLanguage,
|
||||
);
|
||||
|
||||
const countriesSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.countriesCodesList,
|
||||
);
|
||||
|
||||
const editableFieldErrorSelector = createSelector(
|
||||
editableFieldNameSelector,
|
||||
accountSettingsSelector,
|
||||
@@ -211,6 +206,11 @@ const activeAccountSelector = createSelector(
|
||||
accountSettings => accountSettings.values.is_active,
|
||||
);
|
||||
|
||||
const disabledCountriesSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.disabledCountries,
|
||||
);
|
||||
|
||||
export const siteLanguageSelector = createSelector(
|
||||
previousSiteLanguageSelector,
|
||||
draftsSelector,
|
||||
@@ -242,7 +242,7 @@ export const accountSettingsPageSelector = createSelector(
|
||||
mostRecentApprovedVerifiedNameValueSelector,
|
||||
mostRecentVerifiedNameSelector,
|
||||
sortedVerifiedNameHistorySelector,
|
||||
countriesSelector,
|
||||
disabledCountriesSelector,
|
||||
(
|
||||
accountSettings,
|
||||
siteLanguageOptions,
|
||||
@@ -260,7 +260,7 @@ export const accountSettingsPageSelector = createSelector(
|
||||
verifiedName,
|
||||
mostRecentVerifiedName,
|
||||
verifiedNameHistory,
|
||||
countriesCodesList,
|
||||
disabledCountries,
|
||||
) => ({
|
||||
siteLanguageOptions,
|
||||
siteLanguage,
|
||||
@@ -281,7 +281,7 @@ export const accountSettingsPageSelector = createSelector(
|
||||
verifiedName,
|
||||
mostRecentVerifiedName,
|
||||
verifiedNameHistory,
|
||||
countriesCodesList,
|
||||
disabledCountries,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import pick from 'lodash.pick';
|
||||
import omit from 'lodash.omit';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
@@ -8,7 +7,6 @@ import isEmpty from 'lodash.isempty';
|
||||
import { handleRequestError, unpackFieldErrors } from './utils';
|
||||
import { getThirdPartyAuthProviders } from '../third-party-auth';
|
||||
import { postVerifiedNameConfig } from '../certificate-preference/data/service';
|
||||
import { FIELD_LABELS } from './constants';
|
||||
|
||||
const SOCIAL_PLATFORMS = [
|
||||
{ id: 'twitter', key: 'social_link_twitter' },
|
||||
@@ -188,24 +186,6 @@ export async function postVerifiedName(data) {
|
||||
.catch(error => handleRequestError(error));
|
||||
}
|
||||
|
||||
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 getAuthenticatedHttpClient().get(url);
|
||||
return extractCountryList(data);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A single function to GET everything considered a setting. Currently encapsulates Account, Preferences, and
|
||||
* ThirdPartyAuth.
|
||||
@@ -217,14 +197,12 @@ export async function getSettings(username, userRoles) {
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
countries,
|
||||
] = await Promise.all([
|
||||
getAccount(username),
|
||||
getPreferences(username),
|
||||
getThirdPartyAuthProviders(),
|
||||
getProfileDataManager(username, userRoles),
|
||||
getTimeZones(),
|
||||
getCountryList(),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -233,7 +211,6 @@ export async function getSettings(username, userRoles) {
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
countries,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl, createIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
jest.mock('react-dom', () => ({
|
||||
...jest.requireActual('react-dom'),
|
||||
createPortal: jest.fn(node => node), // Mock portal behavior
|
||||
}));
|
||||
ReactDOM.createPortal = node => node;
|
||||
|
||||
import BeforeProceedingBanner from './BeforeProceedingBanner'; // eslint-disable-line import/first
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Modal creates a portal. Overriding createPortal allows portals to be tested in jest.
|
||||
jest.mock('react-dom', () => ({
|
||||
...jest.requireActual('react-dom'),
|
||||
createPortal: jest.fn(node => node), // Mock portal behavior
|
||||
}));
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
|
||||
import { ConfirmationModal } from './ConfirmationModal'; // eslint-disable-line import/first
|
||||
|
||||
|
||||
@@ -76,74 +76,67 @@ export class DeleteAccount extends React.Component {
|
||||
<h2 className="section-heading h4 mb-3">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.header'])}
|
||||
</h2>
|
||||
{
|
||||
this.props.canDeleteAccount ? (
|
||||
<>
|
||||
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages['account.settings.delete.account.text.1'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages[deleteAccountText2MessageKey],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<PrintingInstructions />
|
||||
</p>
|
||||
<p className="text-danger h6">
|
||||
{intl.formatMessage(
|
||||
messages['account.settings.delete.account.text.warning'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<Hyperlink destination="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.text.change.instead'])}
|
||||
</Hyperlink>
|
||||
</p>
|
||||
<p>
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
onClick={canDelete ? this.props.deleteAccountConfirmation : null}
|
||||
disabled={!canDelete}
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.delete.account.button'])}
|
||||
</Button>
|
||||
</p>
|
||||
{isVerifiedAccount ? null : (
|
||||
<BeforeProceedingBanner
|
||||
instructionMessageId={optInInstructionMessageId}
|
||||
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email"
|
||||
/>
|
||||
)}
|
||||
{hasLinkedTPA ? (
|
||||
<BeforeProceedingBanner
|
||||
instructionMessageId="account.settings.delete.account.please.unlink"
|
||||
supportArticleUrl={supportArticleUrl}
|
||||
/>
|
||||
) : null}
|
||||
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages['account.settings.delete.account.text.1'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages[deleteAccountText2MessageKey],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<PrintingInstructions />
|
||||
</p>
|
||||
<p className="text-danger h6">
|
||||
{intl.formatMessage(
|
||||
messages['account.settings.delete.account.text.warning'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<Hyperlink destination="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.text.change.instead'])}
|
||||
</Hyperlink>
|
||||
</p>
|
||||
<p>
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
onClick={canDelete ? this.props.deleteAccountConfirmation : null}
|
||||
disabled={!canDelete}
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.delete.account.button'])}
|
||||
</Button>
|
||||
</p>
|
||||
|
||||
<ConnectedConfirmationModal
|
||||
status={status}
|
||||
errorType={errorType}
|
||||
onSubmit={this.handleSubmit}
|
||||
onCancel={this.handleCancel}
|
||||
onChange={this.handlePasswordChange}
|
||||
password={this.state.password}
|
||||
/>
|
||||
{isVerifiedAccount ? null : (
|
||||
<BeforeProceedingBanner
|
||||
instructionMessageId={optInInstructionMessageId}
|
||||
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email"
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConnectedSuccessModal status={status} onClose={this.handleFinalClose} />
|
||||
</>
|
||||
) : (
|
||||
<p>{intl.formatMessage(messages['account.settings.cannot.delete.account.text'])}</p>
|
||||
)
|
||||
}
|
||||
{hasLinkedTPA ? (
|
||||
<BeforeProceedingBanner
|
||||
instructionMessageId="account.settings.delete.account.please.unlink"
|
||||
supportArticleUrl={supportArticleUrl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<ConnectedConfirmationModal
|
||||
status={status}
|
||||
errorType={errorType}
|
||||
onSubmit={this.handleSubmit}
|
||||
onCancel={this.handleCancel}
|
||||
onChange={this.handlePasswordChange}
|
||||
password={this.state.password}
|
||||
/>
|
||||
|
||||
<ConnectedSuccessModal status={status} onClose={this.handleFinalClose} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -159,7 +152,6 @@ DeleteAccount.propTypes = {
|
||||
errorType: PropTypes.oneOf(['empty-password', 'server']),
|
||||
hasLinkedTPA: PropTypes.bool,
|
||||
isVerifiedAccount: PropTypes.bool,
|
||||
canDeleteAccount: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
@@ -168,7 +160,6 @@ DeleteAccount.defaultProps = {
|
||||
isVerifiedAccount: true,
|
||||
status: null,
|
||||
errorType: null,
|
||||
canDeleteAccount: true,
|
||||
};
|
||||
|
||||
// Assume we're part of the accountSettings state.
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { SuccessModal } from './SuccessModal';
|
||||
|
||||
// Modal creates a portal. Overriding createPortal allows portals to be tested in jest.
|
||||
jest.mock('react-dom', () => ({
|
||||
...jest.requireActual('react-dom'),
|
||||
createPortal: jest.fn(node => node), // Mock portal behavior
|
||||
}));
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
|
||||
import { SuccessModal } from './SuccessModal'; // eslint-disable-line import/first
|
||||
|
||||
const IntlSuccessModal = injectIntl(SuccessModal);
|
||||
|
||||
@@ -22,40 +20,39 @@ describe('SuccessModal', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('should match default closed success modal snapshot', async () => {
|
||||
await waitFor(() => {
|
||||
const tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} /></IntlProvider>)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
await waitFor(() => {
|
||||
const tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="confirming" /></IntlProvider>)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
await waitFor(() => {
|
||||
const tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="pending" /></IntlProvider>)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
await waitFor(() => {
|
||||
const tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="failed" /></IntlProvider>)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
it('should match default closed success modal snapshot', () => {
|
||||
let tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} /></IntlProvider>))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
||||
tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="confirming" /></IntlProvider>))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
||||
tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="pending" /></IntlProvider>))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
||||
tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="failed" /></IntlProvider>))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match open success modal snapshot', async () => {
|
||||
await waitFor(() => {
|
||||
const tree = renderer.create(
|
||||
it('should match open success modal snapshot', () => {
|
||||
const tree = renderer
|
||||
.create((
|
||||
<IntlProvider locale="en">
|
||||
<IntlSuccessModal
|
||||
{...props}
|
||||
status="deleted"
|
||||
status="deleted" // This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
|
||||
/>
|
||||
</IntlProvider>,
|
||||
).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,6 +57,7 @@ exports[`BeforeProceedingBanner should match the snapshot when SUPPORT_URL_TO_UN
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://test-support.edx"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
unlink all social media accounts
|
||||
|
||||
@@ -41,7 +41,7 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
|
||||
/>
|
||||
<div
|
||||
aria-label="Are you sure?"
|
||||
className="pgn__modal pgn__modal-md pgn__modal-default pgn__alert-modal"
|
||||
className="pgn__modal pgn__modal-md pgn__modal-default pgn__modal-visible-overflow pgn__alert-modal"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
@@ -242,7 +242,7 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
|
||||
/>
|
||||
<div
|
||||
aria-label="Are you sure?"
|
||||
className="pgn__modal pgn__modal-md pgn__modal-default pgn__alert-modal"
|
||||
className="pgn__modal pgn__modal-md pgn__modal-default pgn__modal-visible-overflow pgn__alert-modal"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -28,6 +28,7 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Want to change your email, name, or password instead?
|
||||
@@ -74,6 +75,7 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Want to change your email, name, or password instead?
|
||||
@@ -116,6 +118,7 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
activate your account
|
||||
@@ -154,6 +157,7 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Want to change your email, name, or password instead?
|
||||
@@ -196,6 +200,7 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
unlink all social media accounts
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.cannot.delete.account.text': {
|
||||
id: 'account.settings.cannot.delete.account.text',
|
||||
defaultMessage: 'Please note that, for legal and regulatory compliance purposes, account deletion is currently unavailable.',
|
||||
description: 'This text is visible when user is not allowed to delete account',
|
||||
},
|
||||
'account.settings.delete.account.header': {
|
||||
id: 'account.settings.delete.account.header',
|
||||
defaultMessage: 'Delete My Account',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
@@ -12,11 +13,8 @@ import {
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Modal creates a portal. Overriding createPortal allows portals to be tested in jest.
|
||||
jest.mock('react-dom', () => ({
|
||||
...jest.requireActual('react-dom'),
|
||||
createPortal: jest.fn(node => node), // Mock portal behavior
|
||||
}));
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
|
||||
import NameChange from '../NameChange'; // eslint-disable-line import/first
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp, mergeConfig, setConfig } from '@edx/frontend-platform';
|
||||
@@ -37,23 +37,24 @@ describe('JumpNav', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render delete account link', async () => {
|
||||
it('should not render Optional Information or delete account link', () => {
|
||||
setConfig({
|
||||
ENABLE_ACCOUNT_DELETION: false,
|
||||
});
|
||||
|
||||
render(
|
||||
const tree = renderer.create((
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<IntlJumpNav {...props} />
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
|
||||
expect(await screen.queryByText('Delete My Account')).toBeNull();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render delete account link', async () => {
|
||||
it('should render Optional Information and delete account link', () => {
|
||||
setConfig({
|
||||
ENABLE_ACCOUNT_DELETION: true,
|
||||
});
|
||||
@@ -62,14 +63,15 @@ describe('JumpNav', () => {
|
||||
...props,
|
||||
};
|
||||
|
||||
render(
|
||||
const tree = renderer.create((
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<IntlJumpNav {...props} />
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
|
||||
expect(await screen.findByText('Delete My Account')).toBeVisible();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -172,7 +172,9 @@ exports[`EditableSelectField renders EditableSelectField correctly with editing
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div />
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
|
||||
184
src/account-settings/test/__snapshots__/JumpNav.test.jsx.snap
Normal file
184
src/account-settings/test/__snapshots__/JumpNav.test.jsx.snap
Normal file
@@ -0,0 +1,184 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`JumpNav should not render Optional Information or delete account link 1`] = `
|
||||
<div
|
||||
data-testid="redux-provider"
|
||||
>
|
||||
<div
|
||||
data-testid="browser-router"
|
||||
>
|
||||
<div
|
||||
className="jump-nav px-2.25 jump-nav-sm position-sticky pt-3"
|
||||
>
|
||||
<ul
|
||||
className="list-unstyled"
|
||||
style={{}}
|
||||
>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#basic-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Account Information
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#profile-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Profile Information
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#social-media"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Social Media Links
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#site-preferences"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Site Preferences
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#linked-accounts"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Linked Accounts
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`JumpNav should render Optional Information and delete account link 1`] = `
|
||||
<div
|
||||
data-testid="redux-provider"
|
||||
>
|
||||
<div
|
||||
data-testid="browser-router"
|
||||
>
|
||||
<div
|
||||
className="jump-nav px-2.25 jump-nav-sm position-sticky pt-3"
|
||||
>
|
||||
<ul
|
||||
className="list-unstyled"
|
||||
style={{}}
|
||||
>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#basic-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Account Information
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#profile-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Profile Information
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#social-media"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Social Media Links
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#site-preferences"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Site Preferences
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#linked-accounts"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Linked Accounts
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#delete-account"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Delete My Account
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
render, act, screen, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import IdVerificationPageSlot from '../../plugin-slots/IdVerificationPageSlot';
|
||||
import IdVerificationPage from '../IdVerificationPage';
|
||||
import * as selectors from '../data/selectors';
|
||||
|
||||
jest.mock('../data/selectors', () => jest.fn().mockImplementation(() => ({ idVerificationSelector: () => ({}) })));
|
||||
@@ -47,7 +47,7 @@ jest.mock('../panels/SubmittedPanel', () => function SubmittedPanelMock() {
|
||||
return <></>;
|
||||
});
|
||||
|
||||
const IntlIdVerificationPage = injectIntl(IdVerificationPageSlot);
|
||||
const IntlIdVerificationPage = injectIntl(IdVerificationPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('IdVerificationPage', () => {
|
||||
|
||||
@@ -6,13 +6,12 @@ import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
|
||||
import {
|
||||
subscribe, initialize, APP_INIT_ERROR, APP_READY, mergeConfig,
|
||||
} from '@edx/frontend-platform';
|
||||
import React, { StrictMode } from 'react';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Route, Routes, Outlet } from 'react-router-dom';
|
||||
|
||||
import Header from '@edx/frontend-component-header';
|
||||
import { FooterSlot } from '@edx/frontend-component-footer';
|
||||
import FooterSlot from '@openedx/frontend-slot-footer';
|
||||
|
||||
import configureStore from './data/configureStore';
|
||||
import AccountSettingsPage, { NotFoundPage } from './account-settings';
|
||||
@@ -22,39 +21,37 @@ import messages from './i18n';
|
||||
import './index.scss';
|
||||
import Head from './head/Head';
|
||||
|
||||
const rootNode = createRoot(document.getElementById('root'));
|
||||
subscribe(APP_READY, () => {
|
||||
rootNode.render(
|
||||
<StrictMode>
|
||||
<AppProvider store={configureStore()}>
|
||||
<Head />
|
||||
<Routes>
|
||||
<Route element={(
|
||||
<div className="d-flex flex-column" style={{ minHeight: '100vh' }}>
|
||||
<Header />
|
||||
<main className="flex-grow-1" id="main">
|
||||
<Outlet />
|
||||
</main>
|
||||
<FooterSlot />
|
||||
</div>
|
||||
ReactDOM.render(
|
||||
<AppProvider store={configureStore()}>
|
||||
<Head />
|
||||
<Routes>
|
||||
<Route element={(
|
||||
<div className="d-flex flex-column" style={{ minHeight: '100vh' }}>
|
||||
<Header />
|
||||
<main className="flex-grow-1" id="main">
|
||||
<Outlet />
|
||||
</main>
|
||||
<FooterSlot />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Route
|
||||
path="/id-verification/*"
|
||||
element={<IdVerificationPageSlot />}
|
||||
/>
|
||||
<Route path="/" element={<AccountSettingsPage />} />
|
||||
<Route path="/notfound" element={<NotFoundPage />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</AppProvider>
|
||||
</StrictMode>,
|
||||
>
|
||||
<Route
|
||||
path="/id-verification/*"
|
||||
element={<IdVerificationPageSlot />}
|
||||
/>
|
||||
<Route path="/" element={<AccountSettingsPage />} />
|
||||
<Route path="/notfound" element={<NotFoundPage />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</AppProvider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
});
|
||||
|
||||
subscribe(APP_INIT_ERROR, (error) => {
|
||||
rootNode.render(<ErrorPage message={error.message} />);
|
||||
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
|
||||
});
|
||||
|
||||
initialize({
|
||||
@@ -68,7 +65,6 @@ initialize({
|
||||
SHOW_EMAIL_CHANNEL: process.env.SHOW_EMAIL_CHANNEL || 'false',
|
||||
ENABLE_COPPA_COMPLIANCE: (process.env.ENABLE_COPPA_COMPLIANCE || false),
|
||||
ENABLE_ACCOUNT_DELETION: (process.env.ENABLE_ACCOUNT_DELETION !== 'false'),
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED: JSON.parse(process.env.COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED || '[]'),
|
||||
ENABLE_DOB_UPDATE: (process.env.ENABLE_DOB_UPDATE || false),
|
||||
MARKETING_EMAILS_OPT_IN: (process.env.MARKETING_EMAILS_OPT_IN || false),
|
||||
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
import { EMAIL_CADENCE_PREFERENCES, EMAIL_CADENCE } from './data/constants';
|
||||
import EMAIL_CADENCE from './data/constants';
|
||||
import { selectUpdatePreferencesStatus } from './data/selectors';
|
||||
import { LOADING_STATUS } from '../constants';
|
||||
|
||||
@@ -44,12 +44,12 @@ const EmailCadences = ({
|
||||
className="bg-white shadow d-flex flex-column margin-left-2"
|
||||
data-testid="email-cadence-dropdown"
|
||||
>
|
||||
{Object.values(EMAIL_CADENCE_PREFERENCES).map((cadence) => (
|
||||
{Object.values(EMAIL_CADENCE).map((cadence) => (
|
||||
<Dropdown.Item
|
||||
key={cadence}
|
||||
as={Button}
|
||||
variant="tertiary"
|
||||
name={EMAIL_CADENCE}
|
||||
name="email_cadence"
|
||||
className="d-flex justify-content-start py-1.5 font-size-14 cadence-button"
|
||||
size="inline"
|
||||
active={cadence === emailCadence}
|
||||
@@ -71,7 +71,7 @@ const EmailCadences = ({
|
||||
EmailCadences.propTypes = {
|
||||
email: PropTypes.bool.isRequired,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
emailCadence: PropTypes.oneOf(Object.values(EMAIL_CADENCE_PREFERENCES)).isRequired,
|
||||
emailCadence: PropTypes.oneOf(Object.values(EMAIL_CADENCE)).isRequired,
|
||||
notificationType: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,9 +15,6 @@ import { LOADING_STATUS } from '../constants';
|
||||
import { updatePreferenceToggle } from './data/thunks';
|
||||
import { selectAppPreferences, selectSelectedCourseId, selectUpdatePreferencesStatus } from './data/selectors';
|
||||
import { notificationChannels, shouldHideAppPreferences } from './data/utils';
|
||||
import {
|
||||
EMAIL, EMAIL_CADENCE, EMAIL_CADENCE_PREFERENCES, MIXED,
|
||||
} from './data/constants';
|
||||
|
||||
const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -29,34 +26,11 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
const NOTIFICATION_CHANNELS = Object.values(notificationChannels());
|
||||
const hideAppPreferences = shouldHideAppPreferences(appPreferences, appId) || false;
|
||||
|
||||
const getValue = useCallback((notificationChannel, innerText, checked) => {
|
||||
if (notificationChannel === EMAIL_CADENCE && courseId) {
|
||||
return innerText;
|
||||
}
|
||||
return checked;
|
||||
}, [courseId]);
|
||||
|
||||
const getEmailCadence = useCallback((notificationChannel, checked, innerText, emailCadence) => {
|
||||
if (notificationChannel === EMAIL_CADENCE) {
|
||||
return innerText;
|
||||
}
|
||||
if (notificationChannel === EMAIL && checked) {
|
||||
return EMAIL_CADENCE_PREFERENCES.DAILY;
|
||||
}
|
||||
return emailCadence;
|
||||
}, []);
|
||||
|
||||
const onToggle = useCallback((event, notificationType) => {
|
||||
const { name: notificationChannel, checked, innerText } = event.target;
|
||||
const appNotificationPreference = appPreferences.find(preference => preference.id === notificationType);
|
||||
|
||||
const value = getValue(notificationChannel, innerText, checked);
|
||||
const emailCadence = getEmailCadence(
|
||||
notificationChannel,
|
||||
checked,
|
||||
innerText,
|
||||
appNotificationPreference.emailCadence,
|
||||
);
|
||||
const { name: notificationChannel } = event.target;
|
||||
const appNotificationPreference = appPreferences.find(x => x.id === notificationType);
|
||||
const value = notificationChannel === 'email_cadence' && courseId ? event.target.innerText : event.target.checked;
|
||||
const emailCadence = notificationChannel === 'email_cadence' ? event.target.innerText : appNotificationPreference.emailCadence;
|
||||
|
||||
dispatch(updatePreferenceToggle(
|
||||
courseId,
|
||||
@@ -64,9 +38,10 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
emailCadence !== MIXED ? emailCadence : undefined,
|
||||
emailCadence,
|
||||
));
|
||||
}, [appPreferences, getValue, getEmailCadence, dispatch, courseId, appId]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appId, appPreferences]);
|
||||
|
||||
const renderPreference = (preference) => (
|
||||
(preference?.coreNotificationTypes?.length > 0 || preference.id !== 'core') && (
|
||||
@@ -88,7 +63,7 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
id={`${preference.id}-${channel}`}
|
||||
className="my-1"
|
||||
/>
|
||||
{channel === EMAIL && (
|
||||
{channel === 'email' && (
|
||||
<EmailCadences
|
||||
email={preference.email}
|
||||
onToggle={onToggle}
|
||||
|
||||
@@ -27,14 +27,14 @@ const NotificationSettings = () => {
|
||||
</div>
|
||||
<div className="text-gray-700 font-size-14 mb-3">
|
||||
{intl.formatMessage(messages.notificationCadenceDescription, {
|
||||
dailyTime: '22:00 UTC',
|
||||
weeklyTime: '22:00 UTC Every Sunday',
|
||||
dailyTime: '12:00',
|
||||
weeklyTime: '00:00',
|
||||
})}
|
||||
</div>
|
||||
<div className="mb-5 text-gray-700 font-size-14">
|
||||
{intl.formatMessage(messages.notificationPreferenceGuideBody)}
|
||||
<Hyperlink
|
||||
destination="https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/sfd_notifications/index.html"
|
||||
destination="https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/sfd_notifications/managing_notifications.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-decoration-underline ml-1"
|
||||
@@ -44,7 +44,6 @@ const NotificationSettings = () => {
|
||||
</div>
|
||||
<NotificationCoursesDropdown />
|
||||
<NotificationPreferences courseId={courseId} />
|
||||
<div className="border border-light-700 my-6" />
|
||||
</Container>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
export const EMAIL_CADENCE_PREFERENCES = {
|
||||
const EMAIL_CADENCE = {
|
||||
DAILY: 'Daily',
|
||||
WEEKLY: 'Weekly',
|
||||
};
|
||||
export const EMAIL_CADENCE = 'email_cadence';
|
||||
export const EMAIL = 'email';
|
||||
export const MIXED = 'Mixed';
|
||||
export const RequestStatus = /** @type {const} */ ({
|
||||
IN_PROGRESS: 'in-progress',
|
||||
SUCCESSFUL: 'successful',
|
||||
FAILED: 'failed',
|
||||
DENIED: 'denied',
|
||||
PENDING: 'pending',
|
||||
CLEAR: 'clear',
|
||||
PARTIAL: 'partial',
|
||||
PARTIAL_FAILURE: 'partial failure',
|
||||
NOT_FOUND: 'not-found',
|
||||
});
|
||||
|
||||
export default EMAIL_CADENCE;
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import { updatePreferenceToggle } from './thunks';
|
||||
import {
|
||||
updatePreferenceValue,
|
||||
fetchNotificationPreferenceSuccess,
|
||||
fetchNotificationPreferenceFailed,
|
||||
} from './actions';
|
||||
import { patchPreferenceToggle, postPreferenceToggle } from './service';
|
||||
import { EMAIL } from './constants';
|
||||
|
||||
jest.mock('./service', () => ({
|
||||
patchPreferenceToggle: jest.fn(),
|
||||
postPreferenceToggle: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./actions', () => ({
|
||||
updatePreferenceValue: jest.fn(),
|
||||
fetchNotificationPreferenceSuccess: jest.fn(),
|
||||
fetchNotificationPreferenceFailed: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('updatePreferenceToggle', () => {
|
||||
const dispatch = jest.fn();
|
||||
const courseId = 'course-v1:edX+DemoX+2023';
|
||||
const notificationApp = 'app';
|
||||
const notificationType = 'type';
|
||||
const notificationChannel = 'channel';
|
||||
const value = true;
|
||||
const emailCadence = 'daily';
|
||||
|
||||
const mockData = {
|
||||
updated_value: false,
|
||||
notification_type: 'ora_grade_assigned',
|
||||
channel: 'email',
|
||||
app: 'grading',
|
||||
successfully_updated_courses: [
|
||||
{
|
||||
course_id: 'course-v1:ACCA+ColSid+1T2024',
|
||||
current_setting: {
|
||||
web: false,
|
||||
push: false,
|
||||
email: false,
|
||||
email_cadence: 'Weekly',
|
||||
},
|
||||
},
|
||||
{
|
||||
course_id: 'course-v1:arbisoft_acca+cs1023+2021_T4',
|
||||
current_setting: {
|
||||
web: false,
|
||||
push: false,
|
||||
email: false,
|
||||
email_cadence: 'Weekly',
|
||||
},
|
||||
},
|
||||
],
|
||||
total_updated: 2,
|
||||
total_courses: 2,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should update preference for a course-specific notification', async () => {
|
||||
patchPreferenceToggle.mockResolvedValue({ data: mockData });
|
||||
await updatePreferenceToggle(
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
emailCadence,
|
||||
)(dispatch);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(updatePreferenceValue(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
!value,
|
||||
));
|
||||
expect(patchPreferenceToggle).toHaveBeenCalledWith(
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
);
|
||||
expect(dispatch).toHaveBeenCalledWith(fetchNotificationPreferenceSuccess(courseId, { data: mockData }, false));
|
||||
});
|
||||
|
||||
it('should update preference globally when courseId is not provided', async () => {
|
||||
postPreferenceToggle.mockResolvedValue({ data: mockData });
|
||||
await updatePreferenceToggle(
|
||||
null,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
emailCadence,
|
||||
)(dispatch);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(updatePreferenceValue(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
!value,
|
||||
));
|
||||
expect(postPreferenceToggle).toHaveBeenCalledWith(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
emailCadence,
|
||||
);
|
||||
expect(dispatch).toHaveBeenCalledWith(fetchNotificationPreferenceSuccess(null, { data: mockData }, true));
|
||||
});
|
||||
|
||||
it('should handle email preferences separately', async () => {
|
||||
patchPreferenceToggle.mockResolvedValue({ data: mockData });
|
||||
await updatePreferenceToggle(courseId, notificationApp, notificationType, EMAIL, value, emailCadence)(dispatch);
|
||||
|
||||
expect(patchPreferenceToggle).toHaveBeenCalledWith(
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
EMAIL,
|
||||
true,
|
||||
);
|
||||
expect(dispatch).toHaveBeenCalledWith(fetchNotificationPreferenceSuccess(courseId, { data: mockData }, false));
|
||||
});
|
||||
|
||||
it('should dispatch fetchNotificationPreferenceFailed on error', async () => {
|
||||
patchPreferenceToggle.mockRejectedValue(new Error('Network Error'));
|
||||
await updatePreferenceToggle(
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
emailCadence,
|
||||
)(dispatch);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(updatePreferenceValue(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
!value,
|
||||
));
|
||||
expect(dispatch).toHaveBeenCalledWith(fetchNotificationPreferenceFailed());
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import { EMAIL, EMAIL_CADENCE, EMAIL_CADENCE_PREFERENCES } from './constants';
|
||||
import EMAIL_CADENCE from './constants';
|
||||
import {
|
||||
fetchCourseListSuccess,
|
||||
fetchCourseListFetching,
|
||||
@@ -79,8 +79,7 @@ const normalizePreferences = (responseData, courseId) => {
|
||||
push: preferences[appId].notificationTypes[preferenceId].push,
|
||||
email: preferences[appId].notificationTypes[preferenceId].email,
|
||||
info: preferences[appId].notificationTypes[preferenceId].info || '',
|
||||
emailCadence: preferences[appId].notificationTypes[preferenceId].emailCadence
|
||||
|| EMAIL_CADENCE_PREFERENCES.DAILY,
|
||||
emailCadence: preferences[appId].notificationTypes[preferenceId].emailCadence || EMAIL_CADENCE.DAILY,
|
||||
coreNotificationTypes: preferences[appId].coreNotificationTypes || [],
|
||||
}
|
||||
));
|
||||
@@ -140,58 +139,32 @@ export const updatePreferenceToggle = (
|
||||
) => (
|
||||
async (dispatch) => {
|
||||
try {
|
||||
// Initially update the UI to give immediate feedback
|
||||
dispatch(updatePreferenceValue(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
!value,
|
||||
));
|
||||
|
||||
// Function to handle data normalization and dispatching success
|
||||
const handleSuccessResponse = (data, isGlobal = false) => {
|
||||
const processedData = courseId
|
||||
? normalizePreferences(camelCaseObject(data), courseId)
|
||||
: camelCaseObject(data);
|
||||
|
||||
dispatch(fetchNotificationPreferenceSuccess(courseId, processedData, isGlobal));
|
||||
return processedData;
|
||||
};
|
||||
|
||||
// Function to toggle preference based on context (course-specific or global)
|
||||
const togglePreference = async (channel, toggleValue, cadence) => {
|
||||
if (courseId) {
|
||||
return patchPreferenceToggle(
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
channel,
|
||||
channel === EMAIL_CADENCE ? cadence : toggleValue,
|
||||
);
|
||||
}
|
||||
|
||||
return postPreferenceToggle(
|
||||
let data = null;
|
||||
if (courseId) {
|
||||
data = await patchPreferenceToggle(
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
channel,
|
||||
channel === EMAIL_CADENCE ? undefined : toggleValue,
|
||||
cadence,
|
||||
notificationChannel,
|
||||
value,
|
||||
);
|
||||
};
|
||||
|
||||
// Execute the main preference toggle
|
||||
const data = await togglePreference(notificationChannel, value, emailCadence);
|
||||
handleSuccessResponse(data, !courseId);
|
||||
|
||||
// Handle special case for email notifications
|
||||
if (notificationChannel === EMAIL && value) {
|
||||
const emailCadenceData = await togglePreference(
|
||||
EMAIL_CADENCE,
|
||||
courseId ? undefined : value,
|
||||
EMAIL_CADENCE_PREFERENCES.DAILY,
|
||||
const normalizedData = normalizePreferences(camelCaseObject(data), courseId);
|
||||
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
|
||||
} else {
|
||||
data = await postPreferenceToggle(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
emailCadence,
|
||||
);
|
||||
|
||||
handleSuccessResponse(emailCadenceData, !courseId);
|
||||
dispatch(fetchNotificationPreferenceSuccess(courseId, camelCaseObject(data), true));
|
||||
}
|
||||
} catch (errors) {
|
||||
dispatch(updatePreferenceValue(
|
||||
|
||||
@@ -27,9 +27,8 @@ const messages = defineMessages({
|
||||
newQuestionPost {New question posts}
|
||||
contentReported {Reported content}
|
||||
courseUpdates {Course updates}
|
||||
oraStaffNotifications {New ORA submission for staff grading}
|
||||
oraStaffNotification {ORA new submissions}
|
||||
oraGradeAssigned {Essay assignment grade received}
|
||||
newInstructorAllLearnersPost {New posts from instructors}
|
||||
other {{text}}
|
||||
}`,
|
||||
description: 'Display text for Notification Types',
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
# Footer Slot
|
||||
|
||||
### Slot ID: `org.openedx.frontend.layout.footer.v1`
|
||||
|
||||
### Slot ID Aliases
|
||||
* `footer_slot`
|
||||
### Slot ID: `footer_slot`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to replace/modify/hide the footer.
|
||||
|
||||
The implementation of the `FooterSlot` component lives in [the `frontend-component-footer` repository](https://github.com/openedx/frontend-component-footer/).
|
||||
The implementation of the `FooterSlot` component lives in [the `frontend-component-footer` repository](https://github.com/openedx/frontend-component-footer/tree/master/src/components/footer-slot).
|
||||
|
||||
## Example
|
||||
|
||||
@@ -26,7 +23,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.layout.footer.v1': {
|
||||
footer_slot: {
|
||||
plugins: [
|
||||
{
|
||||
// Hide the default footer
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# ID Verification Page Slot
|
||||
# Footer Slot
|
||||
|
||||
### Slot ID: `org.openedx.frontend.account.id_verification_page.v1`
|
||||
|
||||
### Slot ID Aliases
|
||||
* `id_verification_page_plugin`
|
||||
### Slot ID: `id_verification_page_plugin`
|
||||
|
||||
## Description
|
||||
|
||||
@@ -22,13 +19,13 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.account.id_verification_page.v1': {
|
||||
id_verification_page_plugin: {
|
||||
plugins: [
|
||||
{
|
||||
// Insert a custom IDV Page
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_id_verification_page',
|
||||
id: 'id_verification_page_plugin',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: () => (
|
||||
<div>
|
||||
|
||||
@@ -2,10 +2,7 @@ import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import IdVerificationPage from '../../id-verification';
|
||||
|
||||
const IdVerificationPageSlot = () => (
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.account.id_verification_page.v1"
|
||||
idAliases={['id_verification_page_plugin']}
|
||||
>
|
||||
<PluginSlot id="id_verification_page_plugin">
|
||||
<IdVerificationPage />
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# `frontend-app-account` Plugin Slots
|
||||
|
||||
* [`org.openedx.frontend.layout.footer.v1`](./FooterSlot/)
|
||||
* [`org.openedx.frontend.account.id_verification_page.v1`](./IdVerificationPageSlot/)
|
||||
* [`footer_slot`](./FooterSlot/)
|
||||
* [`id_verification_page_plugin`](./IdVerificationPageSlot/)
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('MockedPluginSlot', () => {
|
||||
it('renders as the slot children directly if there is content within', () => {
|
||||
render(
|
||||
<div role="article">
|
||||
<MockedPluginSlot id="test_plugin">
|
||||
<MockedPluginSlot>
|
||||
<q role="note">How much wood could a woodchuck chuck if a woodchuck could chuck wood?</q>
|
||||
</MockedPluginSlot>
|
||||
</div>,
|
||||
@@ -21,13 +21,9 @@ describe('MockedPluginSlot', () => {
|
||||
|
||||
const component = screen.getByRole('article');
|
||||
expect(component).toBeInTheDocument();
|
||||
const slot = component.querySelector('[data-testid="test_plugin"]');
|
||||
expect(slot).toBeInTheDocument();
|
||||
expect(slot).toHaveTextContent('PluginSlot_test_plugin');
|
||||
// Check if the quote is a direct child of the MockedPluginSlot
|
||||
const quote = slot.querySelector('q');
|
||||
expect(quote).toBeInTheDocument();
|
||||
expect(quote).toHaveTextContent('How much wood could a woodchuck chuck if a woodchuck could chuck wood?');
|
||||
|
||||
// Direct children
|
||||
const quote = component.querySelector(':scope > q');
|
||||
expect(quote.getAttribute('role')).toBe('note');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user