Compare commits
37 Commits
release/ul
...
renovate/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a9bf809d1 | ||
|
|
a1c35f134c | ||
|
|
41af76f027 | ||
|
|
57a3b0963c | ||
|
|
92dcdd0a98 | ||
|
|
29f0cefc1e | ||
|
|
794a1d57b9 | ||
|
|
fe46e8a1a6 | ||
|
|
a525d3c22e | ||
|
|
81a2c3c0d2 | ||
|
|
0018eafdcc | ||
|
|
e7db2ef753 | ||
|
|
2e986d9b74 | ||
|
|
7c85195a27 | ||
|
|
b686acf5f5 | ||
|
|
166deeafbd | ||
|
|
f33fe7d0e5 | ||
|
|
e2896dbf94 | ||
|
|
f07d266a43 | ||
|
|
69cdc5f191 | ||
|
|
4f51f71acc | ||
|
|
c70eca1fde | ||
|
|
39dc5bbbd2 | ||
|
|
7fa61f3714 | ||
|
|
8ce7c1599d | ||
|
|
03bdcff331 | ||
|
|
d73d840e93 | ||
|
|
d23b5f53df | ||
|
|
da98bfa021 | ||
|
|
c37640aa69 | ||
|
|
ea35227389 | ||
|
|
c35bf95c1c | ||
|
|
bd26928154 | ||
|
|
cdc8efe17b | ||
|
|
8b6535ea58 | ||
|
|
4c9498971a | ||
|
|
a6b6a3f940 |
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -20,5 +20,5 @@ Include a link to the sandbox for design changes or screenshot for before and af
|
||||
|
||||
#### Post-merge Checklist
|
||||
|
||||
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/edx-infinity** to do it.
|
||||
* [ ] Deploy the changes to prod after verifying on stage or ask **@jacobo-dominguez-wgu** to do it.
|
||||
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: make requirements
|
||||
|
||||
@@ -89,9 +89,9 @@ Cloning and Startup
|
||||
|
||||
``git clone https://github.com/openedx/frontend-app-account.git``
|
||||
|
||||
2. Use node v18.x.
|
||||
2. Use the version of Node specified in the ``.nvmrc`` file.
|
||||
|
||||
The current version of the micro-frontend build scripts support node 18.
|
||||
The current version of the micro-frontend build scripts supports the version of Node found in ``.nvmrc``.
|
||||
Using other major versions of node *may* work, but this is unsupported. For
|
||||
convenience, this repository includes an .nvmrc file to help in setting the
|
||||
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
||||
|
||||
@@ -14,6 +14,6 @@ metadata:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
owner: group:2u-infinity
|
||||
owner: jacobo-dominguez-wgu
|
||||
type: 'website'
|
||||
lifecycle: 'production'
|
||||
|
||||
15
codecov.yml
Normal file
15
codecov.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
enabled: yes
|
||||
target: auto
|
||||
threshold: 0%
|
||||
patch:
|
||||
default:
|
||||
enabled: yes
|
||||
target: auto
|
||||
threshold: 0%
|
||||
ignore:
|
||||
- "src/i18n"
|
||||
- "src/index.jsx"
|
||||
9470
package-lock.json
generated
9470
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -44,9 +44,9 @@
|
||||
"@tensorflow-models/blazeface": "0.1.0",
|
||||
"@tensorflow/tfjs-converter": "4.22.0",
|
||||
"@tensorflow/tfjs-core": "4.22.0",
|
||||
"bowser": "2.12.1",
|
||||
"bowser": "2.14.1",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.46.0",
|
||||
"core-js": "3.48.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"form-urlencoded": "6.1.6",
|
||||
"formdata-polyfill": "4.0.10",
|
||||
@@ -64,7 +64,7 @@
|
||||
"long": "5.3.2",
|
||||
"memoize-one": "^6.0.0",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.14.0",
|
||||
"qs": "6.15.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-helmet": "6.1.0",
|
||||
@@ -77,14 +77,14 @@
|
||||
"redux": "4.2.1",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-saga": "1.3.0",
|
||||
"redux-saga": "1.4.2",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.14.1",
|
||||
"reselect": "^5.1.1",
|
||||
"universal-cookie": "7.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.5.0",
|
||||
"@edx/browserslist-config": "1.5.1",
|
||||
"@openedx/frontend-build": "^14.6.2",
|
||||
"@testing-library/jest-dom": "6.9.1",
|
||||
"@testing-library/react": "14.3.1",
|
||||
|
||||
@@ -764,11 +764,11 @@ class AccountSettingsPage extends React.Component {
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="social_link_twitter"
|
||||
name="social_link_x"
|
||||
type="text"
|
||||
value={this.props.formValues.social_link_twitter}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.twitter'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.twitter.empty'])}
|
||||
value={this.props.formValues.social_link_x}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.xTwitter'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.xTwitter.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</div>
|
||||
@@ -905,7 +905,7 @@ AccountSettingsPage.propTypes = {
|
||||
phone_number: PropTypes.string,
|
||||
social_link_linkedin: PropTypes.string,
|
||||
social_link_facebook: PropTypes.string,
|
||||
social_link_twitter: PropTypes.string,
|
||||
social_link_x: PropTypes.string,
|
||||
time_zone: PropTypes.string,
|
||||
state: PropTypes.string,
|
||||
useVerifiedNameForCerts: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -509,15 +509,15 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Delete My Account',
|
||||
description: 'Header for the user account deletion area',
|
||||
},
|
||||
'account.settings.field.social.platform.name.twitter': {
|
||||
id: 'account.settings.field.social.platform.name.twitter',
|
||||
defaultMessage: 'Twitter',
|
||||
description: 'Label for Twitter',
|
||||
'account.settings.field.social.platform.name.xTwitter': {
|
||||
id: 'account.settings.field.social.platform.name.xTwitter',
|
||||
defaultMessage: 'X (Twitter)',
|
||||
description: 'Label for X (Twitter)',
|
||||
},
|
||||
'account.settings.field.social.platform.name.twitter.empty': {
|
||||
id: 'account.settings.field.social.platform.name.twitter.empty',
|
||||
defaultMessage: 'Add Twitter profile',
|
||||
description: 'Placeholder for an empty Twitter field',
|
||||
'account.settings.field.social.platform.name.xTwitter.empty': {
|
||||
id: 'account.settings.field.social.platform.name.xTwitter.empty',
|
||||
defaultMessage: 'Add X profile',
|
||||
description: 'Placeholder for an empty X field',
|
||||
},
|
||||
|
||||
'account.settings.field.social.platform.name.facebook': {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { postVerifiedNameConfig } from '../certificate-preference/data/service';
|
||||
import { FIELD_LABELS } from './constants';
|
||||
|
||||
const SOCIAL_PLATFORMS = [
|
||||
{ id: 'twitter', key: 'social_link_twitter' },
|
||||
{ id: 'xTwitter', key: 'social_link_x' },
|
||||
{ id: 'facebook', key: 'social_link_facebook' },
|
||||
{ id: 'linkedin', key: 'social_link_linkedin' },
|
||||
];
|
||||
|
||||
@@ -38,24 +38,24 @@ describe('account service', () => {
|
||||
it('returns unpacked account data', async () => {
|
||||
const apiResponse = {
|
||||
username: 'testuser',
|
||||
social_links: [{ platform: 'twitter', social_link: 'http://t' }],
|
||||
social_links: [{ platform: 'xTwitter', social_link: 'http://t' }],
|
||||
language_proficiencies: [{ code: 'en' }],
|
||||
};
|
||||
mockHttpClient.get.mockResolvedValue({ data: apiResponse });
|
||||
|
||||
const result = await getAccount('testuser');
|
||||
expect(mockHttpClient.get).toHaveBeenCalledWith('http://lms.test/api/user/v1/accounts/testuser');
|
||||
expect(result.social_link_twitter).toEqual('http://t');
|
||||
expect(result.social_link_x).toEqual('http://t');
|
||||
expect(result.language_proficiencies).toEqual('en');
|
||||
});
|
||||
});
|
||||
|
||||
describe('patchAccount', () => {
|
||||
it('sends packed commit data and returns unpacked response', async () => {
|
||||
const commit = { social_link_twitter: 'http://t' };
|
||||
const commit = { social_link_x: 'http://t' };
|
||||
const apiResponse = {
|
||||
username: 'testuser',
|
||||
social_links: [{ platform: 'twitter', social_link: 'http://t' }],
|
||||
social_links: [{ platform: 'xTwitter', social_link: 'http://t' }],
|
||||
language_proficiencies: [],
|
||||
};
|
||||
mockHttpClient.patch.mockResolvedValue({ data: apiResponse });
|
||||
@@ -63,10 +63,10 @@ describe('account service', () => {
|
||||
const result = await patchAccount('testuser', commit);
|
||||
expect(mockHttpClient.patch).toHaveBeenCalledWith(
|
||||
'http://lms.test/api/user/v1/accounts/testuser',
|
||||
expect.objectContaining({ social_links: [{ platform: 'twitter', social_link: 'http://t' }] }),
|
||||
expect.objectContaining({ social_links: [{ platform: 'xTwitter', social_link: 'http://t' }] }),
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(result.social_link_twitter).toEqual('http://t');
|
||||
expect(result.social_link_x).toEqual('http://t');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -81,8 +81,8 @@ export class ConfirmationModal extends Component {
|
||||
isOverflowVisible
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button variant="link" onClick={onCancel}>Cancel</Button>
|
||||
<Button variant="danger" onClick={onSubmit}>Yes, Delete</Button>
|
||||
<Button variant="link" onClick={onCancel}>{intl.formatMessage(messages['account.settings.delete.account.modal.confirm.cancel'])}</Button>
|
||||
<Button variant="danger" onClick={onSubmit}>{intl.formatMessage(messages['account.settings.delete.account.modal.confirm.delete'])}</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -38,6 +38,7 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
|
||||
data-testid="modal-backdrop"
|
||||
onClick={[MockFunction]}
|
||||
onKeyDown={[MockFunction]}
|
||||
role="presentation"
|
||||
/>
|
||||
<div
|
||||
aria-label="Are you sure?"
|
||||
@@ -247,15 +248,17 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
tabIndex={0}
|
||||
/>,
|
||||
<div
|
||||
className="pgn__modal-layer"
|
||||
data-focus-lock-disabled="disabled"
|
||||
data-focus-lock-disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
@@ -266,6 +269,7 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
|
||||
data-testid="modal-backdrop"
|
||||
onClick={[MockFunction]}
|
||||
onKeyDown={[MockFunction]}
|
||||
role="presentation"
|
||||
/>
|
||||
<div
|
||||
aria-label="Are you sure?"
|
||||
@@ -393,7 +397,7 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
tabIndex={0}
|
||||
/>,
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -23,15 +23,17 @@ exports[`SuccessModal should match open success modal snapshot 1`] = `
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
tabIndex={0}
|
||||
/>,
|
||||
<div
|
||||
className="pgn__modal-layer"
|
||||
data-focus-lock-disabled="disabled"
|
||||
data-focus-lock-disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
@@ -42,6 +44,7 @@ exports[`SuccessModal should match open success modal snapshot 1`] = `
|
||||
data-testid="modal-backdrop"
|
||||
onClick={[MockFunction]}
|
||||
onKeyDown={[MockFunction]}
|
||||
role="presentation"
|
||||
/>
|
||||
<div
|
||||
className="mw-sm p-5 bg-white mx-auto my-3"
|
||||
@@ -84,7 +87,7 @@ exports[`SuccessModal should match open success modal snapshot 1`] = `
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
tabIndex={0}
|
||||
/>,
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import AccountSettingsPage from '../AccountSettingsPage';
|
||||
import mockData from './mockData';
|
||||
import messages from '../AccountSettingsPage.messages';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
@@ -189,7 +190,7 @@ describe('AccountSettingsPage', () => {
|
||||
|
||||
expect(screen.getByText('https://linkedin.com/in/testuser')).toBeInTheDocument();
|
||||
expect(screen.getByText('Add Facebook profile')).toBeInTheDocument();
|
||||
expect(screen.getByText('Add Twitter profile')).toBeInTheDocument();
|
||||
expect(screen.getByText(messages['account.settings.field.social.platform.name.xTwitter.empty'].defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Site Preferences section with correct field values', () => {
|
||||
|
||||
@@ -71,5 +71,5 @@ export function useFeedbackWrapper() {
|
||||
|
||||
export function useIsOnMobile() {
|
||||
const windowSize = useWindowSize();
|
||||
return windowSize.width <= breakpoints.small.minWidth;
|
||||
return windowSize.width <= breakpoints.small.maxWidth;
|
||||
}
|
||||
|
||||
@@ -65,8 +65,8 @@ initialize({
|
||||
config: () => {
|
||||
mergeConfig({
|
||||
SUPPORT_URL: process.env.SUPPORT_URL,
|
||||
SHOW_PUSH_CHANNEL: process.env.SHOW_PUSH_CHANNEL === 'true',
|
||||
SHOW_EMAIL_CHANNEL: process.env.SHOW_EMAIL_CHANNEL || 'false',
|
||||
SHOW_PUSH_CHANNEL: process.env.SHOW_PUSH_CHANNEL || false,
|
||||
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 || '[]'),
|
||||
|
||||
@@ -18,9 +18,7 @@ import {
|
||||
selectUpdatePreferencesStatus,
|
||||
} from './data/selectors';
|
||||
import { notificationChannels, shouldHideAppPreferences } from './data/utils';
|
||||
import {
|
||||
EMAIL, EMAIL_CADENCE, EMAIL_CADENCE_PREFERENCES, MIXED,
|
||||
} from './data/constants';
|
||||
import { EMAIL, EMAIL_CADENCE } from './data/constants';
|
||||
|
||||
const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -39,13 +37,11 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
return checked;
|
||||
}, []);
|
||||
|
||||
const getEmailCadence = useCallback((notificationChannel, checked, innerText, emailCadence) => {
|
||||
const getEmailCadence = useCallback((notificationChannel, innerText, emailCadence) => {
|
||||
if (notificationChannel === EMAIL_CADENCE) {
|
||||
return innerText;
|
||||
}
|
||||
if (notificationChannel === EMAIL && checked) {
|
||||
return EMAIL_CADENCE_PREFERENCES.DAILY;
|
||||
}
|
||||
|
||||
return emailCadence;
|
||||
}, []);
|
||||
|
||||
@@ -56,7 +52,6 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
const value = getValue(notificationChannel, innerText, checked);
|
||||
const emailCadence = getEmailCadence(
|
||||
notificationChannel,
|
||||
checked,
|
||||
innerText,
|
||||
appNotificationPreference.emailCadence,
|
||||
);
|
||||
@@ -66,12 +61,11 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
emailCadence !== MIXED ? emailCadence : undefined,
|
||||
emailCadence,
|
||||
));
|
||||
}, [appPreferences, getValue, getEmailCadence, dispatch, appId]);
|
||||
|
||||
const renderPreference = (preference) => (
|
||||
(preference?.coreNotificationTypes?.length > 0 || preference.id !== 'core') && (
|
||||
<div
|
||||
key={`${preference.id}-${channel}`}
|
||||
id={`${preference.id}-${channel}`}
|
||||
@@ -100,7 +94,6 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Provider } from 'react-redux';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
import { setConfig } from '@edx/frontend-platform';
|
||||
import { setConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
@@ -110,9 +110,10 @@ describe('Notification Preferences', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
setConfig({
|
||||
mergeConfig({
|
||||
SHOW_EMAIL_CHANNEL: '',
|
||||
});
|
||||
SHOW_PUSH_CHANNEL: '',
|
||||
}, 'App loadConfig override handler');
|
||||
|
||||
store = setupStore({
|
||||
...defaultPreferences,
|
||||
@@ -173,6 +174,59 @@ describe('Notification Preferences', () => {
|
||||
expect(screen.getAllByTestId('email-cadence-button')[0]).toBeDisabled();
|
||||
expect(screen.getByTestId('toggle-newGrade-web')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('does not render push channel when SHOW_PUSH_CHANNEL is false', async () => {
|
||||
setConfig({
|
||||
SHOW_PUSH_CHANNEL: '',
|
||||
});
|
||||
store = setupStore({
|
||||
...defaultPreferences,
|
||||
status: SUCCESS_STATUS,
|
||||
selectedCourse: '',
|
||||
});
|
||||
await render(notificationPreferences(store));
|
||||
|
||||
expect(screen.queryByTestId('toggle-core-push')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders push channel when SHOW_PUSH_CHANNEL is true', async () => {
|
||||
setConfig({
|
||||
SHOW_PUSH_CHANNEL: 'true',
|
||||
});
|
||||
store = setupStore({
|
||||
...defaultPreferences,
|
||||
status: SUCCESS_STATUS,
|
||||
selectedCourse: '',
|
||||
});
|
||||
await render(notificationPreferences(store));
|
||||
expect(screen.queryByTestId('toggle-core-push')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render email channel when SHOW_EMAIL_CHANNEL is false', async () => {
|
||||
setConfig({
|
||||
SHOW_EMAIL_CHANNEL: '',
|
||||
});
|
||||
store = setupStore({
|
||||
...defaultPreferences,
|
||||
status: SUCCESS_STATUS,
|
||||
selectedCourse: '',
|
||||
});
|
||||
await render(notificationPreferences(store));
|
||||
expect(screen.queryByTestId('toggle-core-email')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders email channel when SHOW_EMAIL_CHANNEL is true', async () => {
|
||||
setConfig({
|
||||
SHOW_EMAIL_CHANNEL: 'true',
|
||||
});
|
||||
store = setupStore({
|
||||
...defaultPreferences,
|
||||
status: SUCCESS_STATUS,
|
||||
selectedCourse: '',
|
||||
});
|
||||
await render(notificationPreferences(store));
|
||||
expect(screen.queryByTestId('toggle-core-email')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Notification Preferences API v2 Logic', () => {
|
||||
@@ -195,7 +249,7 @@ describe('Notification Preferences API v2 Logic', () => {
|
||||
|
||||
describe('getNotificationPreferences', () => {
|
||||
it('should call the v2 configurations URL', async () => {
|
||||
const expectedUrl = `${LMS_BASE_URL}/api/notifications/v2/configurations/`;
|
||||
const expectedUrl = `${LMS_BASE_URL}/api/notifications/v3/configurations/`;
|
||||
|
||||
await getNotificationPreferences();
|
||||
|
||||
@@ -206,7 +260,7 @@ describe('Notification Preferences API v2 Logic', () => {
|
||||
|
||||
describe('postPreferenceToggle', () => {
|
||||
it('should call the v2 configurations URL with PUT method', async () => {
|
||||
const expectedUrl = `${LMS_BASE_URL}/api/notifications/v2/configurations/`;
|
||||
const expectedUrl = `${LMS_BASE_URL}/api/notifications/v3/configurations/`;
|
||||
const testArgs = ['app_name', 'notification_type', 'web', true, 'daily'];
|
||||
|
||||
await postPreferenceToggle(...testArgs);
|
||||
|
||||
@@ -23,7 +23,6 @@ const NotificationTypes = ({ appId }) => {
|
||||
return (
|
||||
<div className="d-flex flex-column mr-auto px-0">
|
||||
{preferences.map(preference => (
|
||||
(preference?.coreNotificationTypes?.length > 0 || preference.id !== 'core') && (
|
||||
<>
|
||||
<div
|
||||
key={preference.id}
|
||||
@@ -56,8 +55,6 @@ const NotificationTypes = ({ appId }) => {
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ export const EMAIL_CADENCE_PREFERENCES = {
|
||||
};
|
||||
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',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import snakeCase from 'lodash.snakecase';
|
||||
|
||||
export const getNotificationPreferences = async () => {
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/v2/configurations/`;
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/v3/configurations/`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return data;
|
||||
};
|
||||
@@ -16,13 +16,13 @@ export const postPreferenceToggle = async (
|
||||
emailCadence,
|
||||
) => {
|
||||
const patchData = snakeCaseObject({
|
||||
notificationApp,
|
||||
notificationApp: snakeCase(notificationApp),
|
||||
notificationType: snakeCase(notificationType),
|
||||
notificationChannel,
|
||||
value,
|
||||
emailCadence,
|
||||
});
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/v2/configurations/`;
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/v3/configurations/`;
|
||||
const { data } = await getAuthenticatedHttpClient().put(url, patchData);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('Notification Preferences Service', () => {
|
||||
const result = await getNotificationPreferences();
|
||||
|
||||
expect(mockHttpClient.get).toHaveBeenCalledWith(
|
||||
'http://test.lms/api/notifications/v2/configurations/',
|
||||
'http://test.lms/api/notifications/v3/configurations/',
|
||||
);
|
||||
expect(result).toEqual(mockData);
|
||||
});
|
||||
@@ -50,7 +50,7 @@ describe('Notification Preferences Service', () => {
|
||||
mockHttpClient.put.mockResolvedValue({ data: mockData });
|
||||
|
||||
const result = await postPreferenceToggle(
|
||||
'appName',
|
||||
'app_name',
|
||||
'someType',
|
||||
'email',
|
||||
true,
|
||||
@@ -58,9 +58,9 @@ describe('Notification Preferences Service', () => {
|
||||
);
|
||||
|
||||
expect(mockHttpClient.put).toHaveBeenCalledWith(
|
||||
'http://test.lms/api/notifications/v2/configurations/',
|
||||
'http://test.lms/api/notifications/v3/configurations/',
|
||||
expect.objectContaining({
|
||||
notification_app: 'appName',
|
||||
notification_app: 'app_name',
|
||||
notification_type: 'some_type',
|
||||
notification_channel: 'email',
|
||||
value: true,
|
||||
|
||||
@@ -35,7 +35,7 @@ const normalizePreferences = (responseData) => {
|
||||
const apps = appKeys.map((appId) => ({
|
||||
id: appId,
|
||||
enabled: preferences[appId].enabled,
|
||||
}));
|
||||
})).sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
const nonEditable = {};
|
||||
const preferenceList = appKeys.map(appId => {
|
||||
@@ -50,7 +50,6 @@ const normalizePreferences = (responseData) => {
|
||||
info: preferences[appId].notificationTypes[preferenceId].info || '',
|
||||
emailCadence: preferences[appId].notificationTypes[preferenceId].emailCadence
|
||||
|| EMAIL_CADENCE_PREFERENCES.DAILY,
|
||||
coreNotificationTypes: preferences[appId].coreNotificationTypes || [],
|
||||
}
|
||||
));
|
||||
nonEditable[appId] = preferences[appId].nonEditable;
|
||||
@@ -122,7 +121,7 @@ export const updatePreferenceToggle = (
|
||||
const emailCadenceData = await togglePreference(
|
||||
EMAIL_CADENCE,
|
||||
value,
|
||||
EMAIL_CADENCE_PREFERENCES.DAILY,
|
||||
emailCadence,
|
||||
);
|
||||
|
||||
handleSuccessResponse(emailCadenceData);
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { parseEnvBoolean } from '../../utils';
|
||||
|
||||
export const notificationChannels = () => ({
|
||||
WEB: 'web',
|
||||
...(getConfig().SHOW_PUSH_CHANNEL && { PUSH: 'push' }),
|
||||
...(getConfig().SHOW_EMAIL_CHANNEL === 'true' && { EMAIL: 'email' }),
|
||||
...(parseEnvBoolean(getConfig().SHOW_PUSH_CHANNEL) && { PUSH: 'push' }),
|
||||
...(parseEnvBoolean(getConfig().SHOW_EMAIL_CHANNEL) && { EMAIL: 'email' }),
|
||||
});
|
||||
|
||||
export const shouldHideAppPreferences = (preferences, appId) => {
|
||||
const appPreferences = preferences.filter(pref => pref.appId === appId);
|
||||
|
||||
if (appPreferences.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstPreference = appPreferences[0];
|
||||
|
||||
return firstPreference?.id === 'core' && (!firstPreference.coreNotificationTypes?.length);
|
||||
return appPreferences.length === 0;
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ const messages = defineMessages({
|
||||
id: 'notification.preference.title',
|
||||
defaultMessage: `{
|
||||
text, select,
|
||||
core {Activity notifications}
|
||||
groupedNotification {Activity notifications}
|
||||
newDiscussionPost {New discussion posts}
|
||||
newQuestionPost {New question posts}
|
||||
contentReported {Reported content}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
import '@testing-library/jest-dom';
|
||||
import { initialize, mergeConfig } from '@edx/frontend-platform';
|
||||
import { MockAuthService } from '@edx/frontend-platform/auth';
|
||||
|
||||
import MockedPluginSlot from './tests/MockedPluginSlot';
|
||||
|
||||
@@ -9,3 +11,39 @@ jest.mock('@openedx/frontend-plugin-framework', () => ({
|
||||
Plugin: () => 'Plugin',
|
||||
PluginSlot: MockedPluginSlot,
|
||||
}));
|
||||
|
||||
mergeConfig({
|
||||
SUPPORT_URL: process.env.SUPPORT_URL || 'https://support.example.com',
|
||||
SHOW_PUSH_CHANNEL: process.env.SHOW_PUSH_CHANNEL || false,
|
||||
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 || 'https://support.example.com/password-reset',
|
||||
LEARNER_FEEDBACK_URL: process.env.LEARNER_FEEDBACK_URL || 'https://support.example.com/feedback',
|
||||
}, 'App loadConfig override handler');
|
||||
|
||||
initialize({
|
||||
handlers: {
|
||||
config: () => {
|
||||
mergeConfig({
|
||||
authenticatedUser: {
|
||||
userId: 'abc123',
|
||||
username: 'Mock User',
|
||||
roles: [],
|
||||
administrator: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
messages: [],
|
||||
authService: MockAuthService,
|
||||
});
|
||||
|
||||
global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
10
src/utils.js
10
src/utils.js
@@ -38,3 +38,13 @@ export function getMostRecentApprovedOrPendingVerifiedName(verifiedNames) {
|
||||
|
||||
return applicableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an environment variable string value to a boolean.
|
||||
* @param {string} value the environment variable string value
|
||||
* @returns {boolean} the parsed boolean value
|
||||
*/
|
||||
export const parseEnvBoolean = (value) => {
|
||||
if (!value) { return false; }
|
||||
return String(value).toLowerCase() === 'true';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user