Compare commits

...

54 Commits

Author SHA1 Message Date
edX requirements bot
ddb37580b7 chore: update browserslist DB (#272)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2026-03-16 00:21:05 +00:00
edX requirements bot
30dd3a0ece chore: update browserslist DB (#271)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2026-03-09 00:22:48 +00:00
edX requirements bot
bda9798951 chore: update browserslist DB (#270)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2026-03-02 00:30:12 +00:00
edX requirements bot
87e605cb23 chore: update browserslist DB (#269)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2026-02-23 21:04:02 +00:00
edX requirements bot
35b3de8311 chore: update browserslist DB (#268)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2026-02-16 00:22:34 +00:00
Brian Smith
0c2cf9511f fix(deps): regenerate package-lock.json (#265)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-02-13 17:25:07 -05:00
edX requirements bot
2d6aeb9662 chore: update browserslist DB (#266)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2026-02-09 00:18:38 +00:00
edX requirements bot
8b84cfdf27 chore: update browserslist DB (#264)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2026-01-26 00:14:35 +00:00
Anton Melser
6223212751 docs: updates for outdated references (#262)
* docs: Generify currently supported node version

* docs: include 'standard' dev instructions

---------

Co-authored-by: Feanil Patel <feanil@axim.org>
2026-01-23 18:36:33 +00:00
Feanil Patel
4a346328ef build: Update package-lock.json
The file was missing some dependent packages for some reason.
2026-01-23 15:28:58 -03:00
PKulkoRaccoonGang
5d19b93d98 fix: added raw loader for tinymce conten.css 2026-01-19 15:14:02 -03:00
edX requirements bot
42e75a6372 chore: update browserslist DB (#261)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-11-10 00:13:49 +00:00
edX requirements bot
ac5fa76c1e chore: update browserslist DB (#258)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-11-06 13:22:05 -05:00
Stanislav Lunyachek
7ddc95003e fix: Remove extra margin around body element 2025-09-30 12:16:04 -03:00
Muhammad Anas
fa05fa25ab chore: bump frontend-component-header to v6.6.x 2025-09-30 11:55:12 -03:00
oleksandr.buhaienko
26099ea6d5 test: Remove support for Node 20 2025-09-26 10:35:39 -03:00
Feanil Patel
59dbee3fa9 Merge pull request #256 from openedx/feanil/remove-reactifex-packages
build: remove unused @edx/reactifex package
2025-09-25 13:15:44 -04:00
Feanil Patel
1c9e20e6a7 fix: Correct test parameters.
This test should be suppling a `courseModes` parameter not a
`courseMode` parameter with a missing `s`.
2025-09-25 10:13:24 -04:00
Feanil Patel
4af3a5a65a build: remove unused @edx/reactifex package
Remove @edx/reactifex package from devDependencies as it is no longer
needed. Translation extraction functionality has been verified to work
correctly without this dependency.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 10:13:18 -04:00
PKulkoRaccoonGang
b106d0694f fix: fixed some problems with tests 2025-09-25 10:55:05 -03:00
oleksandr.buhaienko
ef6c498bb7 build: Upgrade to Node 24 2025-09-25 10:55:05 -03:00
edX requirements bot
70a40bf90b chore: update browserslist DB (#255)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-09-22 00:13:18 +00:00
bydawen
7727574280 test: Add Node 24 to CI matrix (#252) 2025-09-19 13:50:59 -04:00
edX requirements bot
51b8d7bac1 chore: update browserslist DB (#251)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-09-08 00:13:07 +00:00
Samuel Allan
95a3eb4959 fix: update frontend-build to fix install issues (#250)
Earlier versions of @openedx/frontend-build used on older version of
'sharp', which caused intermittent installation issues. The version of
'sharp' was updated in @openedx/frontend-build to fix these issues, so
the frontend-build version can be updated here, to fix the issues in
this project too. See
https://github.com/openedx/frontend-build/issues/664 and
https://github.com/openedx/frontend-build/pull/665 for more information.

The frontend-build dependency was updated by:

```
npm install --package-lock-only @openedx/frontend-build
```

Private-ref: https://tasks.opencraft.com/browse/BB-9953
2025-09-05 12:03:05 -06:00
edX requirements bot
975ab436ae chore: update browserslist DB (#249)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-09-01 00:14:46 +00:00
edX requirements bot
64b259a8a9 chore: update browserslist DB (#248)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-08-25 00:13:19 +00:00
edX requirements bot
795636f7a7 chore: update browserslist DB (#247)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-08-18 00:14:21 +00:00
Jacobo Dominguez
f8e2b3de03 refactor: replacing injectIntl with useIntl part 1 (#245) 2025-08-13 13:03:13 -04:00
Jacobo Dominguez
b5e4505665 refactor: replacing injectIntl with useIntl part 2 (#246) 2025-08-13 10:51:53 -04:00
edX requirements bot
74905663e1 chore: update browserslist DB (#241)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-07-07 00:14:01 +00:00
edX requirements bot
a1083d8142 chore: update browserslist DB (#240)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-06-30 00:13:54 +00:00
Brian Smith
20ef9002ec feat!: add design tokens support (#224)
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.

Co-authored-by: Diana Olarte <dcoa@live.com>
2025-06-18 15:36:53 -04:00
edX requirements bot
d2cb5b5e1d chore: update browserslist DB (#237)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-06-16 00:13:32 +00:00
edX requirements bot
cd73b9992f chore: update browserslist DB (#235)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-06-09 00:14:00 +00:00
edX requirements bot
dd99ad7c57 chore: update browserslist DB (#234)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-06-02 00:13:44 +00:00
Brian Smith
f6dfc7f6cc fix(deps): update dependency @edx/frontend-platform to v8.3.7 (#232) 2025-05-19 11:06:25 -04:00
edX requirements bot
c3d9b62944 chore: update browserslist DB (#231)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-05-19 00:13:27 +00:00
edX requirements bot
c0a6133e78 chore: update browserslist DB (#230)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-05-12 00:13:22 +00:00
edX requirements bot
d62aa1df5a chore: update browserslist DB (#229)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-05-05 00:13:21 +00:00
Brian Smith
0c1eb6cae0 feat: import FooterSlot from component package instead of slot package (#226) 2025-04-24 12:10:52 -04:00
Brian Smith
16738335d0 fix(deps): update frontend-component-header to ^6.4.0 (#228) 2025-04-23 17:05:36 -04:00
edX requirements bot
658b70e455 chore: update browserslist DB (#227)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-04-21 00:13:06 +00:00
edX requirements bot
6cb174b146 chore: update browserslist DB (#225)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-04-14 00:13:07 +00:00
edX requirements bot
143f0dcd4b chore: update browserslist DB (#222)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-04-07 00:12:44 +00:00
Brian Smith
ad19426aee feat: upgrade to react 18 (#219) 2025-04-04 13:46:15 -04:00
Régis Behmo
5ab646b69c chore: remove husky 🪓🐶 (#220) 2025-04-04 05:13:54 -04:00
Brian Smith
1cd02a9dfb chore: update @openedx dependencies to versions that support React 18 (#218) 2025-03-27 16:15:55 -04:00
Feanil Patel
445a2f6cd3 Merge pull request #215 from salman2013/salman/update-catalog-info-file
Update catalog-info file for release data
2025-02-11 14:26:35 -05:00
salman2013
166b6fe7ae fix: remove openedx.yaml file 2025-02-11 13:56:57 +05:00
salman2013
a6711a59dc chore: update catalog-info file for release data 2025-01-31 17:11:48 +05:00
Muhammad Anas
454d3ddcdf test: Remove support for Node 18 (#213) 2024-10-31 14:44:39 -04:00
Brian Smith
e4a7448850 feat(deps): update header to 5.6.0 (#214) 2024-10-22 19:19:01 -04:00
Muhammad Anas
1fd3343355 build: Upgrade to Node 20 (#211)
* build: Upgrade to Node 20

* refactor: updated package-lock

* refactor: updated the lockfile version workflow
2024-09-06 12:23:17 -04:00
36 changed files with 10030 additions and 5268 deletions

2
.env
View File

@@ -21,3 +21,5 @@ USER_INFO_COOKIE_NAME=''
SCHEDULE_EMAIL_SECTION=''
APP_ID=''
MFE_CONFIG_API_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -22,3 +22,5 @@ USER_INFO_COOKIE_NAME='edx-user-info'
SCHEDULE_EMAIL_SECTION='true'
APP_ID=''
MFE_CONFIG_API_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -20,3 +20,4 @@ USER_INFO_COOKIE_NAME='edx-user-info'
SCHEDULE_EMAIL_SECTION='true'
APP_ID=''
MFE_CONFIG_API_URL=''
PARAGON_THEME_URLS={}

View File

@@ -9,10 +9,7 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20]
continue-on-error: ${{ matrix.node == 20 }}
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -23,7 +20,7 @@ jobs:
- name: Setup Nodejs
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci
- name: Validate package-lock.json changes

View File

@@ -10,4 +10,4 @@ on:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ node_modules
npm-debug.log
coverage
module.config.js
env.config.*
dist/
src/i18n/transifex_input.json

3
.nvmrc
View File

@@ -1,2 +1 @@
18
24

View File

@@ -5,14 +5,24 @@ frontend-app-communications
Purpose
*******
********
A tool used by course teams to communicate with their learners. The interface for anything related to instructor-to-learner communications. Instructor bulk email, for example.
Getting started
---------------
Getting Started
***************
Prerequisites
=============
`Tutor`_ is currently recommended as a development environment for your
new MFE. Please refer
to the `relevant tutor-mfe documentation`_ to get started using it.
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
For now, this repo is not integrated with devstack. You'll be running the app locally and not through docker. This does make setup a little easier.
Cloning and Startup
===================
@@ -23,9 +33,9 @@ Cloning and Startup
git clone https://github.com/edx/frontend-app-communications.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 supports 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 a ``.nvmrc`` file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.

View File

@@ -12,6 +12,7 @@ metadata:
icon: "Article"
annotations:
openedx.org/arch-interest-groups: ""
openedx.org/release: "master"
spec:
owner: group:committers-frontend
type: "service"

View File

@@ -1,9 +0,0 @@
# This file describes this Open edX repo, as described in OEP-2:
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
oeps: {}
openedx-release:
# The openedx-release key is described in OEP-10:
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0010-proc-openedx-releases.html
# The FAQ might also be helpful: https://openedx.atlassian.net/wiki/spaces/COMM/pages/1331268879/Open+edX+Release+FAQ
ref: master

14613
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,17 +12,13 @@
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"lint": "fedx-scripts eslint --ext .js --ext .jsx src/",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx src/",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/communications/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/edx/frontend-app-communications#readme",
@@ -34,8 +30,9 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "5.3.0",
"@edx/frontend-platform": "^8.0.0",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.6.1",
"@edx/frontend-platform": "^8.3.7",
"@edx/openedx-atlas": "^0.6.0",
"@edx/tinymce-language-selector": "1.1.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
@@ -43,9 +40,8 @@
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.2.0",
"@openedx/frontend-plugin-framework": "^1.1.2",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.0.0",
"@openedx/frontend-plugin-framework": "^1.6.0",
"@openedx/paragon": "^23.3.0",
"@tinymce/tinymce-react": "3.14.0",
"axios": "0.27.2",
"classnames": "2.3.2",
@@ -53,8 +49,8 @@
"jquery": "3.6.1",
"popper.js": "1.16.1",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-redux": "7.2.9",
"react-router": "6.15.0",
@@ -65,13 +61,12 @@
},
"devDependencies": {
"@edx/browserslist-config": "^1.2.0",
"@openedx/frontend-build": "14.0.3",
"@edx/reactifex": "^2.1.1",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@edx/typescript-config": "^1.1.0",
"@openedx/frontend-build": "^14.6.2",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"axios-mock-adapter": "1.21.2",
"glob": "7.2.3",
"husky": "7.0.4",
"jest": "29.7.0",
"prettier": "2.8.1",
"rosie": "2.1.0"

View File

@@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
<UNDEFINED>
<ErrorPage
message="test-error-message"
/>
</UNDEFINED>
`;
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
<UNDEFINED>
<AppProvider>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
>
<link
href="favicon-url"
rel="shortcut icon"
type="image/x-icon"
/>
</HelmetWrapper>
<Routes>
<Route
element={
<AuthenticatedPageRoute>
<Page Container>
<Bulk Email Tool />
</Page Container>
</AuthenticatedPageRoute>
}
path="/courses/:courseId/bulk_email"
/>
</Routes>
</AppProvider>
</UNDEFINED>
`;

View File

@@ -8,7 +8,7 @@ import {
import {
SpinnerSimple, Cancel, Send, Event, Check,
} from '@openedx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import classNames from 'classnames';
import { getConfig } from '@edx/frontend-platform';
import TextEditor from '../text-editor/TextEditor';
@@ -51,8 +51,8 @@ function BulkEmailForm(props) {
courseId,
cohorts,
courseModes,
intl,
} = props;
const intl = useIntl();
const [{ editor }, dispatch] = useContext(BulkEmailContext);
const [emailFormStatus, setEmailFormStatus] = useState(FORM_SUBMIT_STATES.DEFAULT);
const [emailFormValidation, setEmailFormValidation] = useState({
@@ -392,7 +392,7 @@ BulkEmailForm.defaultProps = {
BulkEmailForm.propTypes = {
courseId: PropTypes.string.isRequired,
cohorts: PropTypes.arrayOf(PropTypes.string),
intl: intlShape.isRequired,
courseModes: PropTypes.arrayOf(
PropTypes.shape({
slug: PropTypes.string.isRequired,
@@ -401,4 +401,4 @@ BulkEmailForm.propTypes = {
).isRequired,
};
export default injectIntl(BulkEmailForm);
export default BulkEmailForm;

View File

@@ -39,7 +39,7 @@ function renderBulkEmailForm() {
function renderBulkEmailFormContext(value) {
return (
<BulkEmailContext.Provider value={[value, dispatchMock]}>
<BulkEmailForm courseId="test" courseMode={courseMode} />
<BulkEmailForm courseId="test" courseModes={courseMode} />
</BulkEmailContext.Provider>
);
}

View File

@@ -3,7 +3,7 @@
import React, { useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { useParams } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Button, Collapsible, Icon,
@@ -14,7 +14,8 @@ import { getSentEmailHistory } from './data/api';
import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
import ViewEmailModal from './ViewEmailModal';
function BulkEmailContentHistory({ intl }) {
function BulkEmailContentHistory() {
const intl = useIntl();
const { courseId } = useParams();
const [emailHistoryData, setEmailHistoryData] = useState();
const [errorRetrievingData, setErrorRetrievingData] = useState(false);
@@ -154,7 +155,6 @@ function BulkEmailContentHistory({ intl }) {
}
BulkEmailContentHistory.propTypes = {
intl: intlShape.isRequired,
row: PropTypes.shape({
index: PropTypes.number,
}),
@@ -164,4 +164,4 @@ BulkEmailContentHistory.defaultProps = {
row: {},
};
export default injectIntl(BulkEmailContentHistory);
export default BulkEmailContentHistory;

View File

@@ -1,13 +1,14 @@
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getInstructorTasks } from './data/api';
import messages from './messages';
import useInterval from '../../../utils/useInterval';
import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
function BulkEmailPendingTasks({ intl }) {
function BulkEmailPendingTasks() {
const intl = useIntl();
const { courseId } = useParams();
const [instructorTaskData, setInstructorTaskData] = useState();
@@ -89,8 +90,4 @@ function BulkEmailPendingTasks({ intl }) {
);
}
BulkEmailPendingTasks.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(BulkEmailPendingTasks);
export default BulkEmailPendingTasks;

View File

@@ -1,6 +1,6 @@
import React, { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon, Collapsible } from '@openedx/paragon';
import { SpinnerSimple } from '@openedx/paragon/icons';
@@ -11,7 +11,8 @@ import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
import './bulkEmailTaskHistory.scss';
function BulkEmailTaskHistory({ intl }) {
function BulkEmailTaskHistory() {
const intl = useIntl();
const { courseId } = useParams();
const [emailTaskHistoryData, setEmailTaskHistoryData] = useState([]);
@@ -117,8 +118,4 @@ function BulkEmailTaskHistory({ intl }) {
);
}
BulkEmailTaskHistory.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(BulkEmailTaskHistory);
export default BulkEmailTaskHistory;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import BulkEmailContentHistory from './BulkEmailContentHistory';
import BulkEmailTaskHistory from './BulkEmailTaskHistory';
@@ -9,7 +9,8 @@ import messages from './messages';
import BulkEmailScheduledEmailsTable from './bulk-email-scheduled-emails-table';
import BulkEmailPendingTasksAlert from './BulkEmailPendingTasksAlert';
function BulkEmailTaskManager({ intl, courseId }) {
function BulkEmailTaskManager({ courseId }) {
const intl = useIntl();
return (
<div className="w-100">
{getConfig().SCHEDULE_EMAIL_SECTION && (
@@ -34,8 +35,7 @@ function BulkEmailTaskManager({ intl, courseId }) {
}
BulkEmailTaskManager.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
};
export default injectIntl(BulkEmailTaskManager);
export default BulkEmailTaskManager;

View File

@@ -1,14 +1,15 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { ActionRow, Button, ModalDialog } from '@openedx/paragon';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
import { BulkEmailContext } from '../bulk-email-context';
import { copyToEditor } from '../bulk-email-form/data/actions';
function ViewEmailModal({
intl, messageContent, isOpen, setModalOpen,
messageContent, isOpen, setModalOpen,
}) {
const intl = useIntl();
const [, dispatch] = useContext(BulkEmailContext);
return (
<div>
@@ -72,7 +73,6 @@ function ViewEmailModal({
}
ViewEmailModal.propTypes = {
intl: intlShape.isRequired,
messageContent: PropTypes.shape({
subject: PropTypes.string,
requester: PropTypes.string,
@@ -86,4 +86,4 @@ ViewEmailModal.propTypes = {
setModalOpen: PropTypes.func.isRequired,
};
export default injectIntl(ViewEmailModal);
export default ViewEmailModal;

View File

@@ -4,7 +4,7 @@
import React, {
useCallback, useContext, useState, useEffect,
} from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Alert, DataTable, Icon, IconButton, useToggle,
} from '@openedx/paragon';
@@ -32,7 +32,8 @@ function flattenScheduledEmailsArray(emails) {
}));
}
function BulkEmailScheduledEmailsTable({ intl }) {
function BulkEmailScheduledEmailsTable() {
const intl = useIntl();
const { courseId } = useParams();
const [{ scheduledEmailsTable }, dispatch] = useContext(BulkEmailContext);
const [tableData, setTableData] = useState([]);
@@ -196,8 +197,4 @@ function BulkEmailScheduledEmailsTable({ intl }) {
);
}
BulkEmailScheduledEmailsTable.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(BulkEmailScheduledEmailsTable);
export default BulkEmailScheduledEmailsTable;

View File

@@ -3,7 +3,7 @@
*/
import React from 'react';
import {
render, screen, fireEvent, cleanup, act, initializeMockApp,
render, screen, fireEvent, cleanup, initializeMockApp,
} from '../../../../setupTest';
import { BulkEmailProvider } from '../../bulk-email-context';
import BulkEmailContentHistory from '../BulkEmailContentHistory';
@@ -41,107 +41,99 @@ describe('BulkEmailContentHistory component', () => {
});
test('renders a table when the button is pressed and data is returned', async () => {
await act(async () => {
const emailHistoryData = buildEmailContentHistoryData(1);
getSentEmailHistory.mockImplementation(() => emailHistoryData);
const emailHistoryData = buildEmailContentHistoryData(1);
getSentEmailHistory.mockImplementation(() => emailHistoryData);
render(renderBulkEmailContentHistory());
render(renderBulkEmailContentHistory());
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
// verify component structure
const tableDescription = await screen.findByText(
'To read a sent email message, click the `View Message` button within the table.',
);
expect(tableDescription).toBeTruthy();
// verify component structure
const tableDescription = await screen.findByText(
'To read a sent email message, click the `View Message` button within the table.',
);
expect(tableDescription).toBeTruthy();
// verify table structure
expect(await screen.findByText('Subject')).toBeTruthy();
expect(await screen.findByText('Sent By')).toBeTruthy();
expect(await screen.findByText('Sent To')).toBeTruthy();
expect(await screen.findByText('Time Sent')).toBeTruthy();
expect(await screen.findByText('Number Sent')).toBeTruthy();
// verify table structure
expect(await screen.findByText('Subject')).toBeTruthy();
expect(await screen.findByText('Sent By')).toBeTruthy();
expect(await screen.findByText('Sent To')).toBeTruthy();
expect(await screen.findByText('Time Sent')).toBeTruthy();
expect(await screen.findByText('Number Sent')).toBeTruthy();
// verify table contents
const { emails } = emailHistoryData;
const email = emails[0];
const createdDate = new Date(email.created).toLocaleString();
expect(await screen.findByText(createdDate)).toBeTruthy();
expect(await screen.findByText(email.number_sent)).toBeTruthy();
expect(await screen.findByText(email.requester)).toBeTruthy();
expect(await screen.findByText(email.sent_to.join(', '))).toBeTruthy();
expect(await screen.findByText(email.email.subject)).toBeTruthy();
// verify screen reader only <span />
expect(await screen.findByText('0')).toHaveClass('sr-only');
expect(await screen.findAllByText('View Message')).toBeTruthy();
});
// verify table contents
const { emails } = emailHistoryData;
const email = emails[0];
const createdDate = new Date(email.created).toLocaleString();
expect(await screen.findByText(createdDate)).toBeTruthy();
expect(await screen.findByText(email.number_sent)).toBeTruthy();
expect(await screen.findByText(email.requester)).toBeTruthy();
expect(await screen.findByText(email.sent_to.join(', '))).toBeTruthy();
expect(await screen.findByText(email.email.subject)).toBeTruthy();
// verify screen reader only <span />
expect(await screen.findByText('0')).toHaveClass('sr-only');
expect(await screen.findAllByText('View Message')).toBeTruthy();
});
test('renders a modal that will display the contents of the previously sent message to a user', async () => {
await act(async () => {
const emailHistoryData = buildEmailContentHistoryData(1);
getSentEmailHistory.mockImplementation(() => emailHistoryData);
const emailHistoryData = buildEmailContentHistoryData(1);
getSentEmailHistory.mockImplementation(() => emailHistoryData);
render(renderBulkEmailContentHistory());
render(renderBulkEmailContentHistory());
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
const viewMessageButton = await screen.findByText('View Message');
fireEvent.click(viewMessageButton);
const viewMessageButton = await screen.findByText('View Message');
fireEvent.click(viewMessageButton);
// verify modal components and behavior
const { emails } = emailHistoryData;
const email = emails[0];
const closeButton = await screen.findAllByText('Close');
// verify modal components and behavior
const { emails } = emailHistoryData;
const email = emails[0];
const closeButton = await screen.findAllByText('Close');
expect(closeButton).toBeTruthy();
expect(await screen.findByText('Subject:')).toBeTruthy();
expect(await screen.findByText('Sent by:')).toBeTruthy();
expect(await screen.findByText('Time sent:')).toBeTruthy();
expect(await screen.findByText('Sent to:')).toBeTruthy();
expect(await screen.findByText('Message:')).toBeTruthy();
expect(await screen.findAllByText(email.email.subject)).toBeTruthy();
expect(await screen.findAllByText(email.requester)).toBeTruthy();
const createdDate = new Date(email.created).toLocaleString();
expect(await screen.findAllByText(createdDate)).toBeTruthy();
expect(await screen.findAllByText(email.sent_to.join(', '))).toBeTruthy();
// .replace() call strips the HTML tags from the string
expect(await screen.findByText(email.email.html_message.replace(/<[^>]*>?/gm, ''))).toBeTruthy();
});
expect(closeButton).toBeTruthy();
expect(await screen.findByText('Subject:')).toBeTruthy();
expect(await screen.findByText('Sent by:')).toBeTruthy();
expect(await screen.findByText('Time sent:')).toBeTruthy();
expect(await screen.findByText('Sent to:')).toBeTruthy();
expect(await screen.findByText('Message:')).toBeTruthy();
expect(await screen.findAllByText(email.email.subject)).toBeTruthy();
expect(await screen.findAllByText(email.requester)).toBeTruthy();
const createdDate = new Date(email.created).toLocaleString();
expect(await screen.findAllByText(createdDate)).toBeTruthy();
expect(await screen.findAllByText(email.sent_to.join(', '))).toBeTruthy();
// .replace() call strips the HTML tags from the string
expect(await screen.findByText(email.email.html_message.replace(/<[^>]*>?/gm, ''))).toBeTruthy();
});
test('renders a warning Alert when the button is pressed but there is no data to display', async () => {
await act(async () => {
const emailHistoryData = buildEmailContentHistoryData(0);
getSentEmailHistory.mockImplementation(() => emailHistoryData);
// render the component
render(renderBulkEmailContentHistory());
// press the `show sent email history` button to initiate data retrieval
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText('There is no email history for this course.');
expect(alertMessage).toBeTruthy();
});
const emailHistoryData = buildEmailContentHistoryData(0);
getSentEmailHistory.mockImplementation(() => emailHistoryData);
// render the component
render(renderBulkEmailContentHistory());
// press the `show sent email history` button to initiate data retrieval
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText('There is no email history for this course.');
expect(alertMessage).toBeTruthy();
});
test('renders an error Alert when the button is pressed and an error occurs retrieving data', async () => {
await act(async () => {
getSentEmailHistory.mockImplementation(() => {
throw new Error();
});
// render the component
render(renderBulkEmailContentHistory());
// press the `show sent email history` button to initiate data retrieval
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText(
'An error occurred retrieving email history data for this course. Please try again later.',
);
expect(alertMessage).toBeTruthy();
getSentEmailHistory.mockImplementation(() => {
throw new Error();
});
// render the component
render(renderBulkEmailContentHistory());
// press the `show sent email history` button to initiate data retrieval
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText(
'An error occurred retrieving email history data for this course. Please try again later.',
);
expect(alertMessage).toBeTruthy();
});
});

View File

@@ -3,7 +3,7 @@
*/
import React from 'react';
import {
render, screen, fireEvent, cleanup, act, initializeMockApp,
render, screen, fireEvent, cleanup, initializeMockApp,
} from '../../../../setupTest';
import BulkEmailTaskHistory from '../BulkEmailTaskHistory';
import { getEmailTaskHistory } from '../data/api';
@@ -32,72 +32,66 @@ describe('BulkEmailTaskHistory component', () => {
});
test('renders a table properly when the button is pressed and data is returned', async () => {
await act(async () => {
// build our mocked response
const taskHistoryData = buildEmailTaskHistoryData(1);
getEmailTaskHistory.mockImplementation(() => taskHistoryData);
// render the component
render(<BulkEmailTaskHistory />);
// press the `show task history button` to initiate data retrieval and rendering of the table in our component
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
fireEvent.click(showTaskHistoryButton);
// verification of table structure
expect(await screen.findByText('Task Type')).toBeTruthy();
expect(await screen.findByText('Task Inputs')).toBeTruthy();
expect(await screen.findByText('Task Id')).toBeTruthy();
expect(await screen.findByText('Requester')).toBeTruthy();
expect(await screen.findByText('Submitted')).toBeTruthy();
expect(await screen.findByText('Duration (seconds)')).toBeTruthy();
expect(await screen.findByText('State')).toBeTruthy();
expect(await screen.findByText('Status')).toBeTruthy();
expect(await screen.findByText('Task Progress')).toBeTruthy();
expect(await screen.findAllByText('Showing 1 - 1 of 1.')).toBeTruthy();
// verification of row contents
const { tasks } = taskHistoryData;
const task = tasks[0];
const createdDate = new Date(task.created).toLocaleString();
expect(await screen.findByText(createdDate)).toBeTruthy();
expect(await screen.findByText(task.duration_sec)).toBeTruthy();
expect(await screen.findByText(task.requester)).toBeTruthy();
expect(await screen.findByText(task.status)).toBeTruthy();
expect(await screen.findByText(task.task_id)).toBeTruthy();
expect(await screen.findByText(task.task_input)).toBeTruthy();
expect(await screen.findByText(task.task_message)).toBeTruthy();
expect(await screen.findByText(task.task_state)).toBeTruthy();
expect(await screen.findByText(task.task_type)).toBeTruthy();
});
// build our mocked response
const taskHistoryData = buildEmailTaskHistoryData(1);
getEmailTaskHistory.mockImplementation(() => taskHistoryData);
// render the component
render(<BulkEmailTaskHistory />);
// press the `show task history button` to initiate data retrieval and rendering of the table in our component
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
fireEvent.click(showTaskHistoryButton);
// verification of table structure
expect(await screen.findByText('Task Type')).toBeTruthy();
expect(await screen.findByText('Task Inputs')).toBeTruthy();
expect(await screen.findByText('Task Id')).toBeTruthy();
expect(await screen.findByText('Requester')).toBeTruthy();
expect(await screen.findByText('Submitted')).toBeTruthy();
expect(await screen.findByText('Duration (seconds)')).toBeTruthy();
expect(await screen.findByText('State')).toBeTruthy();
expect(await screen.findByText('Status')).toBeTruthy();
expect(await screen.findByText('Task Progress')).toBeTruthy();
expect(await screen.findAllByText('Showing 1 - 1 of 1.')).toBeTruthy();
// verification of row contents
const { tasks } = taskHistoryData;
const task = tasks[0];
const createdDate = new Date(task.created).toLocaleString();
expect(await screen.findByText(createdDate)).toBeTruthy();
expect(await screen.findByText(task.duration_sec)).toBeTruthy();
expect(await screen.findByText(task.requester)).toBeTruthy();
expect(await screen.findByText(task.status)).toBeTruthy();
expect(await screen.findByText(task.task_id)).toBeTruthy();
expect(await screen.findByText(task.task_input)).toBeTruthy();
expect(await screen.findByText(task.task_message)).toBeTruthy();
expect(await screen.findByText(task.task_state)).toBeTruthy();
expect(await screen.findByText(task.task_type)).toBeTruthy();
});
test('renders a warning Alert when the button is pressed but there is no data to display', async () => {
await act(async () => {
const taskHistoryData = buildEmailTaskHistoryData(0);
getEmailTaskHistory.mockImplementation(() => taskHistoryData);
// render the component
render(<BulkEmailTaskHistory />);
// press the `show task history` button to initiate data retrieval
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
fireEvent.click(showTaskHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText('There is no email task history for this course.');
expect(alertMessage).toBeTruthy();
});
const taskHistoryData = buildEmailTaskHistoryData(0);
getEmailTaskHistory.mockImplementation(() => taskHistoryData);
// render the component
render(<BulkEmailTaskHistory />);
// press the `show task history` button to initiate data retrieval
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
fireEvent.click(showTaskHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText('There is no email task history for this course.');
expect(alertMessage).toBeTruthy();
});
test('renders an error Alert when the button is pressed and an error occurs retrieving data', async () => {
await act(async () => {
getEmailTaskHistory.mockImplementation(() => {
throw new Error();
});
// render the component
render(<BulkEmailTaskHistory />);
// press the `show task history` button to initiate data retrieval
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
fireEvent.click(showTaskHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText(
'Error fetching email task history data for this course. Please try again later.',
);
expect(alertMessage).toBeTruthy();
getEmailTaskHistory.mockImplementation(() => {
throw new Error();
});
// render the component
render(<BulkEmailTaskHistory />);
// press the `show task history` button to initiate data retrieval
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
fireEvent.click(showTaskHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText(
'Error fetching email task history data for this course. Please try again later.',
);
expect(alertMessage).toBeTruthy();
});
});

View File

@@ -1,13 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ActionRow, AlertModal, Button } from '@openedx/paragon';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
function TaskAlertModal(props) {
const {
isOpen, close, alertMessage, intl,
isOpen, close, alertMessage,
} = props;
const intl = useIntl();
const messages = {
taskAlertTitle: {
id: 'bulk.email.task.alert.title',
@@ -57,7 +57,6 @@ TaskAlertModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
close: PropTypes.func.isRequired,
alertMessage: PropTypes.node.isRequired,
intl: intlShape.isRequired,
};
export default injectIntl(TaskAlertModal);
export default TaskAlertModal;

View File

@@ -17,8 +17,8 @@ import 'tinymce/plugins/image';
import 'tinymce/plugins/codesample';
import '@edx/tinymce-language-selector';
import contentUiCss from 'tinymce/skins/ui/oxide/content.css';
import contentCss from 'tinymce/skins/content/default/content.css';
import contentUiCss from 'tinymce/skins/ui/oxide/content.css?raw';
import contentCss from 'tinymce/skins/content/default/content.css?raw';
export default function TextEditor(props) {
const {

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { useParams } from 'react-router-dom';
import { LearningHeader as Header } from '@edx/frontend-component-header';
import FooterSlot from '@openedx/frontend-slot-footer';
import { FooterSlot } from '@edx/frontend-component-footer';
import { Spinner } from '@openedx/paragon';
import { getCohorts, getCourseHomeCourseMetadata } from './data/api';

View File

@@ -4,7 +4,7 @@
import React from 'react';
import { Factory } from 'rosie';
import {
act, cleanup, initializeMockApp, render, screen,
cleanup, initializeMockApp, render, screen,
} from '../../../setupTest';
import PageContainer from '../PageContainer';
@@ -32,36 +32,32 @@ describe('PageContainer', () => {
afterEach(cleanup);
test('PageContainer renders properly when given course metadata', async () => {
await act(async () => {
const cohorts = { cohorts: [Factory.build('cohort'), Factory.build('cohort')] };
const courseMetadata = Factory.build('courseMetadata');
const cohorts = { cohorts: [Factory.build('cohort'), Factory.build('cohort')] };
const courseMetadata = Factory.build('courseMetadata');
getCohorts.mockImplementation(() => cohorts);
getCourseHomeCourseMetadata.mockImplementation(() => courseMetadata);
getCohorts.mockImplementation(() => cohorts);
getCourseHomeCourseMetadata.mockImplementation(() => courseMetadata);
render(<PageContainer />);
render(<PageContainer />);
// Look for the org, title, and number of the course, which should be displayed in the Header.
expect(await screen.findByText(`${courseMetadata.org} ${courseMetadata.number}`)).toBeTruthy();
expect(await screen.findByText(courseMetadata.title)).toBeTruthy();
});
// Look for the org, title, and number of the course, which should be displayed in the Header.
expect(await screen.findByText(`${courseMetadata.org} ${courseMetadata.number}`)).toBeTruthy();
expect(await screen.findByText(courseMetadata.title)).toBeTruthy();
});
test('PageContainer renders children nested within it.', async () => {
await act(async () => {
const cohorts = { cohorts: [Factory.build('cohort'), Factory.build('cohort')] };
const courseMetadata = Factory.build('courseMetadata');
const cohorts = { cohorts: [Factory.build('cohort'), Factory.build('cohort')] };
const courseMetadata = Factory.build('courseMetadata');
getCohorts.mockImplementation(() => cohorts);
getCourseHomeCourseMetadata.mockImplementation(() => courseMetadata);
getCohorts.mockImplementation(() => cohorts);
getCourseHomeCourseMetadata.mockImplementation(() => courseMetadata);
render(
<PageContainer>
<span>Test Text</span>
</PageContainer>,
);
render(
<PageContainer>
<span>Test Text</span>
</PageContainer>,
);
expect(await screen.findByText('Test Text')).toBeTruthy();
});
expect(await screen.findByText('Test Text')).toBeTruthy();
});
});

View File

@@ -5,7 +5,8 @@ import {
APP_INIT_ERROR, APP_READY, subscribe, initialize, mergeConfig, getConfig,
} from '@edx/frontend-platform';
import { AppProvider, AuthenticatedPageRoute, ErrorPage } from '@edx/frontend-platform/react';
import ReactDOM from 'react-dom';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Helmet } from 'react-helmet';
import { Routes, Route } from 'react-router-dom';
@@ -16,30 +17,39 @@ import BulkEmailTool from './components/bulk-email-tool';
import PageContainer from './components/page-container/PageContainer';
subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider>
<Helmet>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
<Routes>
<Route
path="/courses/:courseId/bulk_email"
element={(
<AuthenticatedPageRoute>
<PageContainer>
<BulkEmailTool />
</PageContainer>
</AuthenticatedPageRoute>
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<AppProvider>
<Helmet>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
<Routes>
<Route
path="/courses/:courseId/bulk_email"
element={(
<AuthenticatedPageRoute>
<PageContainer>
<BulkEmailTool />
</PageContainer>
</AuthenticatedPageRoute>
)}
/>
</Routes>
</AppProvider>,
document.getElementById('root'),
/>
</Routes>
</AppProvider>
</StrictMode>,
);
});
subscribe(APP_INIT_ERROR, (error) => {
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<ErrorPage message={error.message} />
</StrictMode>,
);
});
initialize({

View File

@@ -1,7 +1,4 @@
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/footer";

67
src/index.test.jsx Normal file
View File

@@ -0,0 +1,67 @@
import {
APP_INIT_ERROR, APP_READY, subscribe,
} from '@edx/frontend-platform';
// Jest needs this for module resolution
import * as app from '.'; // eslint-disable-line @typescript-eslint/no-unused-vars
// These need to be var not let so they get hoisted
// and can be used by jest.mock (which is also hoisted)
var mockRender; // eslint-disable-line no-var
var mockCreateRoot; // eslint-disable-line no-var
jest.mock('react-dom/client', () => {
mockRender = jest.fn();
mockCreateRoot = jest.fn(() => ({
render: mockRender,
}));
return ({
createRoot: mockCreateRoot,
});
});
jest.mock('@edx/frontend-platform', () => ({
APP_READY: 'app-is-ready-key',
APP_INIT_ERROR: 'app-init-error',
subscribe: jest.fn(),
initialize: jest.fn(),
mergeConfig: jest.fn(),
getConfig: () => ({
FAVICON_URL: 'favicon-url',
}),
ensureConfig: jest.fn(),
}));
jest.mock('./components/bulk-email-tool/BulkEmailTool', () => 'Bulk Email Tool');
jest.mock('./components/page-container/PageContainer', () => 'Page Container');
describe('app registry', () => {
let getElement;
beforeEach(() => {
mockCreateRoot.mockClear();
mockRender.mockClear();
getElement = window.document.getElementById;
window.document.getElementById = jest.fn(id => ({ id }));
});
afterAll(() => {
window.document.getElementById = getElement;
});
test('subscribe: APP_READY. links App to root element', () => {
const callArgs = subscribe.mock.calls[0];
expect(callArgs[0]).toEqual(APP_READY);
callArgs[1]();
const [rendered] = mockRender.mock.calls[0];
expect(rendered).toMatchSnapshot();
});
test('subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element', () => {
const callArgs = subscribe.mock.calls[1];
expect(callArgs[0]).toEqual(APP_INIT_ERROR);
const error = { message: 'test-error-message' };
callArgs[1](error);
const [rendered] = mockRender.mock.calls[0];
expect(rendered).toMatchSnapshot();
});
});

View File

@@ -1,12 +1,15 @@
# Footer Slot
### Slot ID: `footer_slot`
### Slot ID: `org.openedx.frontend.layout.footer.v1`
### Slot ID Aliases
* `footer_slot`
## Description
This slot is used to replace/modify/hide the footer.
The implementation of the `FooterSlot` component lives in [the `frontend-slot-footer` repository](https://github.com/openedx/frontend-slot-footer/).
The implementation of the `FooterSlot` component lives in [the `frontend-component-footer` repository](https://github.com/openedx/frontend-component-footer/).
## Example
@@ -23,7 +26,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = {
pluginSlots: {
footer_slot: {
'org.openedx.frontend.layout.footer.v1': {
plugins: [
{
// Hide the default footer

View File

@@ -1,3 +1,3 @@
# `frontend-app-communications` Plugin Slots
* [`footer_slot`](./FooterSlot/)
* [`org.openedx.frontend.layout.footer.v1`](./FooterSlot/)

View File

@@ -13,6 +13,7 @@ import messages from './i18n';
jest.mock('@edx/frontend-platform/react/hooks', () => ({
...jest.requireActual('@edx/frontend-platform/react/hooks'),
useTrackColorSchemeChoice: jest.fn(),
useParagonTheme: () => [{ isThemeLoaded: true }, jest.fn()],
}));
Object.defineProperty(window, 'matchMedia', {
@@ -57,11 +58,6 @@ export function initializeMockApp() {
return { loggingService, i18nService, authService };
}
jest.mock('@edx/frontend-platform/react/hooks', () => ({
...jest.requireActual('@edx/frontend-platform/react/hooks'),
useTrackColorSchemeChoice: jest.fn(),
}));
function render(ui, options) {
// eslint-disable-next-line react/prop-types
function Wrapper({ children }) {

13
tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "@edx/typescript-config",
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"*": ["*"]
},
"rootDir": ".",
"outDir": "dist"
},
"include": ["*.js", ".eslintrc.js", "src/**/*", "plugins/**/*", "jest.config.ts"],
"exclude": ["*.js", ".eslintrc.js", "dist", "node_modules"]
}

View File

@@ -22,8 +22,13 @@ const webpack5esmInteropRule = {
},
};
const rawAssetRule = {
resourceQuery: /raw/,
type: 'asset/source',
};
const otherRules = config.module.rules;
config.module.rules = [webpack5esmInteropRule, ...otherRules];
config.module.rules = [rawAssetRule, webpack5esmInteropRule, ...otherRules];
module.exports = config;

View File

@@ -14,8 +14,13 @@ const webpack5esmInteropRule = {
},
};
const rawAssetRule = {
resourceQuery: /raw/,
type: 'asset/source',
};
const otherRules = config.module.rules;
config.module.rules = [webpack5esmInteropRule, ...otherRules];
config.module.rules = [rawAssetRule, webpack5esmInteropRule, ...otherRules];
module.exports = config;