Compare commits

...

79 Commits

Author SHA1 Message Date
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
Muhammad Anas
ef6ade6de1 test: Add Node 20 to CI matrix (#210) 2024-09-03 14:31:42 -04:00
Braden MacDonald
7dc08c060f chore: remove reference to deprecated frontend-lib-content-components repo (#209) 2024-09-03 10:00:30 -07:00
Bilal Qamar
1e1f269b6b feat: updated frontend-build & frontend-platform major versions (#181)
* chore: bumped jest to v29

* refactor: updated frontend-build & added overrides

* feat: updated build and platform major versions, along with edx packages

* refactor: added caret to frontend-platform version
2024-08-06 15:36:25 +05:00
Emad Rad
a8093439a9 fix: update README.rst (#208)
Ffix typos, add code-blocks and highlighting for better readability
2024-07-24 11:51:29 -07:00
Adolfo R. Brandes
10f906b72f build: Update codecov and use token
Update codecov to the latest version and start using the org-wide token for uploads.

See https://github.com/openedx/wg-frontend/issues/179
2024-06-17 12:02:35 -03:00
Brian Smith
3a14119d01 fix: import FooterSlot from frontend-slot-footer package 2024-05-17 09:37:27 -03:00
Brian Smith
cb3b2b4670 feat: use frontend-plugin-framework to provide a FooterSlot 2024-05-09 16:54:37 -03:00
sundasnoreen12
9279a3e4ce Merge pull request #200 from openedx/sundas/INF-1278
feat: remove Transifex calls for OEP-58 for communication app
2024-03-22 14:15:34 +05:00
sundasnoreen12
2a5cf010f8 feat: remove Transifex calls for OEP-58 for communication app 2024-03-21 23:23:59 +05:00
eemaanamir
6dd835d63f refactor: converted functions to memos for best practice 2024-03-06 09:15:44 -03:00
eemaanamir
87c1cb5bd2 test: updated tests for both components to accommodate changes 2024-03-06 09:15:44 -03:00
eemaanamir
5875631a6c refactor: updated code according to best practices 2024-03-06 09:15:44 -03:00
eemaanamir
176a95c352 fix: converted UTC time to localtime in all the bulk email history tables 2024-03-06 09:15:44 -03:00
Kyle McCormick
b24971572d build: update catalog-info team frontend-all to committers-frontend
per https://openedx.atlassian.net/wiki/spaces/COMM/pages/3555852316/GitHub+Access+Team+Structure
2024-03-01 13:00:31 -03:00
Taras Lytvynenko
20f212501f fix: Added maxLength and default tip for email subject (#154)
* fix: Added maxLength and default tip for email subject

* fix: lint
2024-02-27 15:14:56 -03:00
Eugene Dyudyunov
852d0ef2d9 refactor: apply review suggestions 2024-02-27 15:00:14 -03:00
Taras Lytvynenko
809112d4b2 docs: Unnecessary comment deleted 2024-02-27 15:00:14 -03:00
Taras Lytvynenko
d249b5e4aa fix: Logical operations moved into a separate variable 2024-02-27 15:00:14 -03:00
Taras Lytvynenko
dad6c84d46 fix: Course mode is used to show the correct bulk email options 2024-02-27 15:00:14 -03:00
Brian Smith
5f61578e28 chore(deps): update paragon and frontend-build to openedx scope (#190) 2024-01-22 17:23:16 -05:00
Omar Al-Ithawi
da30730662 chore: bump openedx-atlas==0.6.0 (#189) 2024-01-17 11:50:29 -05:00
Omar Al-Ithawi
6e4ae9d976 feat: add ATLAS_OPTIONS and lib-components and frontend-platform deps (#187) 2024-01-09 11:51:56 -05:00
arbrandes
e863eef77d chore: update browserslist DB 2023-12-15 14:57:36 -03:00
arbrandes
ff900335a1 chore: update browserslist DB 2023-12-04 11:23:40 -03:00
arbrandes
c0dc0cbad6 chore: update browserslist DB 2023-11-28 11:42:45 -03:00
arbrandes
83aec7318e chore: update browserslist DB 2023-11-20 12:31:27 -03:00
Ihor Romaniuk
a037f5b340 fix: date format depends on locale date format (#175) 2023-11-16 07:05:57 -03:00
Omar Al-Ithawi
91402fbf12 feat!: remove broken transifex and use atlas exclusively | FC-0012 (#164)
* feat!: remove broken transifex and use atlas exclusively

* feat: install openedx-atlas
2023-11-15 15:10:50 -05:00
o.bugaenko
7a299eb064 fix: wrong id placement on the h1 tag 2023-11-14 16:13:56 -03:00
arbrandes
90e8eeeb50 chore: update browserslist DB 2023-11-14 13:04:20 -03:00
vladislavkeblysh
f25ce0b95d feat: fixed layout 2023-11-14 12:32:37 -03:00
Stanislav Lunyachek
98db1a4a35 fix: Missed favicon in Safari 2023-11-14 12:23:58 -03:00
Muhammad Abdullah Waheed
cf4b632c55 feat: babel-plugin-react-intl to babel-plugin-formatjs migration (#151)
* feat: babel-plugin-react-intl to babel-plugin-formatjs migration

* fix: upgraded frontend-build to fix security issue
2023-11-14 12:58:07 +05:00
Mashal Malik
0720b2feae refactor: add @openedx in renovate automate configuration (#150) 2023-11-01 12:38:32 -03:00
Jason Wesson
56e1781004 Merge pull request #145 from openedx/update-browserslist-db
Update browserslist DB
2023-10-23 13:07:18 -07:00
arbrandes
83eb21bb9a chore: update browserslist DB 2023-10-23 00:07:35 +00:00
Feanil Patel
18ddb35e1e chore: Update to the new version of brand-openedx in the new scope. (#165)
Part of https://github.com/openedx/axim-engineering/issues/23

This updates the `@edx/brand` alias to point to the `brand-openedx` package at
the `openedx` scope. This does not impact imports because this package is used
via an alias.
2023-10-20 17:26:49 -04:00
Syed Ali Abbas Zaidi
ae93b68d57 chore: bump frontend-platform (#158) 2023-10-12 18:41:58 +05:00
69 changed files with 11495 additions and 11017 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

@@ -1,6 +1,6 @@
/* eslint-disable import/no-extraneous-dependencies */
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');
module.exports = createConfig('eslint', {
rules: {

View File

@@ -18,9 +18,9 @@ jobs:
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci
- name: Validate package-lock.json changes
@@ -34,4 +34,7 @@ jobs:
- name: i18n_extract
run: npm run i18n_extract
- name: Coverage
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true

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

2
.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
@@ -17,3 +18,4 @@ temp/babel-plugin-react-intl
*~
/temp
/.vscode
src/i18n/messages/

3
.nvmrc
View File

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

View File

@@ -1,8 +0,0 @@
[main]
host = https://www.transifex.com
[o:open-edx:p:edx-platform:r:frontend-app-communications]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -1,20 +1,17 @@
export TRANSIFEX_RESOURCE = frontend-app-communications
transifex_langs = "ar,fr,es_419,zh_CN"
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl
transifex_temp = ./temp/babel-plugin-formatjs
precommit:
npm run lint
npm audit
requirements:
npm install
npm ci
i18n.extract:
# Pulling display strings from .jsx files into .json files...
@@ -32,35 +29,18 @@ 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
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull --filter=$(transifex_langs) \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-app-communications/src/i18n/messages:frontend-app-communications
&& atlas pull $(ATLAS_OPTIONS) \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-platform/src/i18n/messages:frontend-platform \
translations/frontend-app-communications/src/i18n/messages:frontend-app-communications
$(intl_imports) frontend-component-header frontend-component-footer paragon frontend-app-communications
endif
$(intl_imports) frontend-component-header frontend-component-footer paragon frontend-platform frontend-app-communications
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:

View File

@@ -1,5 +1,5 @@
frontend-app-communications
#############################
###########################
|license-badge| |status-badge| |ci-badge| |codecov-badge|
@@ -7,52 +7,56 @@ 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.
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
------------
---------------
For now, this repo is not intergrated with devstack. You'll be running the app locally and not through docker. This does make setup a little easier.
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
===================
1. Clone your new repo:
1. Clone your new repo:
``git clone https://github.com/edx/frontend-app-communications.git``
.. code-block:: bash
2. Use node v18.x.
git clone https://github.com/edx/frontend-app-communications.git
The current version of the micro-frontend build scripts support node 18.
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>`_.
2. Use node v18.x.
3. Install npm dependencies:
The current version of the micro-frontend build scripts supports node 18.
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>`_.
``cd frontend-app-communications && npm install``
3. Install npm dependencies:
4. Update the application port to use for local development:
.. code-block:: bash
Default port is 1984. If this does not work for you, update the line
`PORT=1984` to your port in all .env.* files
cd frontend-app-communications && npm install
5. Start the devserver. The app will be running at ``localhost:1984``, or whatever port you change it too.
4. Update the application port to use for local development:
.. code-block::
The default port is 1984. If this does not work for you, update the line
``PORT=1984`` to your port in all ``.env.*`` files
npm start
5. Start the devserver. The app will be running at ``localhost:1984``, or whatever port you change it too.
.. code-block:: bash
npm start
Environment Variables/Setup Notes
---------------------------------
If you wish to add new environment varibles for local testing, they should be listed in 2 places:
If you wish to add new environment variables for local testing, they should be listed in 2 places:
1. In ``.env.development``
2. Added to the ``mergeConfig`` found in ``src/index.jsx``
.. code-block::
.. code-block:: jsx
initialize({
config: () => {
@@ -61,14 +65,19 @@ If you wish to add new environment varibles for local testing, they should be li
}, 'CommuncationsAppConfig');
Running Tests
---------------------------
-------------
Tests use `jest` and `react-test-library`. To run all the tests for this repo:
.. code-block::
.. code-block::
npm test
npm test
Plugins
=======
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
**Production Build**
@@ -77,7 +86,7 @@ The production build is created with ``npm run build``.
Internationalization
====================
Please see refer to the `frontend-platform i18n howto`_ for documentation on
Please refer to the `frontend-platform i18n howto`_ for documentation on
internationalization.
.. _frontend-platform i18n howto: https://github.com/openedx/frontend-platform/blob/master/docs/how_tos/i18n.rst
@@ -160,4 +169,4 @@ Please do not report security issues in public, and email security@openedx.org i
.. |codecov-badge| image:: https://codecov.io/github/openedx/frontend-app-communications/coverage.svg?branch=master
:target: https://codecov.io/github/openedx/frontend-app-communications?branch=master
:alt: Codecov
:alt: Codecov

View File

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

View File

@@ -1,4 +1,4 @@
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');
module.exports = createConfig('jest', {
// setupFilesAfterEnv is used after the jest environment has been loaded. In general this is what you want.

View File

@@ -23,9 +23,9 @@ module.exports = {
**********************************************************************************************/
// { moduleName: '@edx/brand', dir: '../brand-openedx' }, // replace with your brand checkout
// { moduleName: '@edx/paragon/scss/core', dir: '../paragon', dist: 'scss/core' },
// { moduleName: '@edx/paragon/icons', dir: '../paragon', dist: 'icons' },
// { moduleName: '@edx/paragon', dir: '../paragon', dist: 'dist' },
// { moduleName: '@openedx/paragon/scss/core', dir: '../paragon', dist: 'scss/core' },
// { moduleName: '@openedx/paragon/icons', dir: '../paragon', dist: 'icons' },
// { moduleName: '@openedx/paragon', dir: '../paragon', dist: 'dist' },
// { moduleName: '@edx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
],
};

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

21405
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,18 +11,14 @@
],
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"i18n_extract": "fedx-scripts formatjs extract",
"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",
@@ -33,17 +29,19 @@
"url": "https://github.com/edx/frontend-app-communications/issues"
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-footer": "12.2.0",
"@edx/frontend-component-header": "4.6.0",
"@edx/frontend-platform": "5.0.0",
"@edx/paragon": "^20.44.0",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@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",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@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.6.0",
"@openedx/paragon": "^23.3.0",
"@tinymce/tinymce-react": "3.14.0",
"axios": "0.27.2",
"classnames": "2.3.2",
@@ -51,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",
@@ -63,14 +61,13 @@
},
"devDependencies": {
"@edx/browserslist-config": "^1.2.0",
"@edx/frontend-build": "^12.7.0",
"@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": "27.5.1",
"jest": "29.7.0",
"prettier": "2.8.1",
"rosie": "2.1.0"
}

View File

@@ -4,6 +4,7 @@
<title>Communications | <%= process.env.SITE_NAME %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
</head>
<body>
<div id="root"></div>

View File

@@ -24,7 +24,7 @@
"automerge": true
},
{
"matchPackagePatterns": ["@edx"],
"matchPackagePatterns": ["@edx", "@openedx"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}

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

@@ -3,7 +3,7 @@ import React from 'react';
import { useParams } from 'react-router-dom';
import { ErrorPage } from '@edx/frontend-platform/react';
import { Container } from '@edx/paragon';
import { Container } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import BulkEmailTaskManager from './bulk-email-task-manager/BulkEmailTaskManager';
import NavigationTabs from '../navigation-tabs/NavigationTabs';
@@ -24,7 +24,7 @@ export default function BulkEmailTool() {
<Container size="md">
<BackToInstructor courseId={courseId} />
<div className="row pb-4.5">
<h1 className="text-primary-500" id="main-content">
<h1 className="text-primary-500">
<FormattedMessage
id="bulk.email.send.email.header"
defaultMessage="Send an email"
@@ -33,7 +33,11 @@ export default function BulkEmailTool() {
</h1>
</div>
<div className="row">
<BulkEmailForm courseId={courseId} cohorts={courseMetadata.cohorts} />
<BulkEmailForm
courseId={courseId}
cohorts={courseMetadata.cohorts}
courseModes={courseMetadata.courseModes}
/>
</div>
<div className="row py-5">
<BulkEmailTaskManager courseId={courseId} />

View File

@@ -4,11 +4,11 @@ import PropTypes from 'prop-types';
import {
Button,
Form, Icon, StatefulButton, Toast, useToggle,
} from '@edx/paragon';
} from '@openedx/paragon';
import {
SpinnerSimple, Cancel, Send, Event, Check,
} from '@edx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
} from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import classNames from 'classnames';
import { getConfig } from '@edx/frontend-platform';
import TextEditor from '../text-editor/TextEditor';
@@ -47,7 +47,12 @@ const FORM_ACTIONS = {
};
function BulkEmailForm(props) {
const { courseId, cohorts, intl } = props;
const {
courseId,
cohorts,
courseModes,
} = props;
const intl = useIntl();
const [{ editor }, dispatch] = useContext(BulkEmailContext);
const [emailFormStatus, setEmailFormStatus] = useState(FORM_SUBMIT_STATES.DEFAULT);
const [emailFormValidation, setEmailFormValidation] = useState({
@@ -272,10 +277,14 @@ function BulkEmailForm(props) {
handleCheckboxes={onRecipientChange}
additionalCohorts={cohorts}
isValid={emailFormValidation.recipients}
courseModes={courseModes}
/>
<Form.Group controlId="emailSubject">
<Form.Label className="h3 text-primary-500">{intl.formatMessage(messages.bulkEmailSubjectLabel)}</Form.Label>
<Form.Control name="emailSubject" className="w-lg-50" onChange={onFormChange} value={editor.emailSubject} />
<Form.Control name="emailSubject" className="w-lg-50" onChange={onFormChange} value={editor.emailSubject} maxLength={128} />
<Form.Control.Feedback className="px-3" type="default">
{intl.formatMessage(messages.bulkEmailFormSubjectTip)}
</Form.Control.Feedback>
{!emailFormValidation.subject && (
<Form.Control.Feedback className="px-3" hasIcon type="invalid">
{intl.formatMessage(messages.bulkEmailFormSubjectError)}
@@ -383,7 +392,13 @@ 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,
name: PropTypes.string.isRequired,
}),
).isRequired,
};
export default injectIntl(BulkEmailForm);
export default BulkEmailForm;

View File

@@ -2,13 +2,15 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Form } from '@edx/paragon';
import { Form } from '@openedx/paragon';
import useMobileResponsive from '../../../utils/useMobileResponsive';
function ScheduleEmailForm(props) {
const isMobile = useMobileResponsive();
const { isValid, onDateTimeChange, dateTime } = props;
const { date, time } = dateTime;
const descriptionDate = new Date();
descriptionDate.setDate(new Date().getDate() + 1);
return (
<Form.Group>
<div className={classNames('d-flex', isMobile ? 'flex-column' : 'flex-row', 'my-3')}>
@@ -30,7 +32,10 @@ function ScheduleEmailForm(props) {
<small className="text-gray-500 x-small">
<FormattedMessage
id="bulk.email.form.schedule.date.description"
defaultMessage="Enter a start date, e.g. 11/27/2023"
defaultMessage="Enter a start date, e.g. {date}"
values={{
date: descriptionDate.toLocaleDateString(),
}}
/>
</small>
</div>
@@ -52,7 +57,10 @@ function ScheduleEmailForm(props) {
<small className="text-gray-500 x-small">
<FormattedMessage
id="bulk.email.form.schedule.time.description"
defaultMessage="Enter a start time, e.g. 09:00 AM"
defaultMessage="Enter a start time, e.g. {time}"
values={{
time: descriptionDate.toLocaleTimeString([], { timeStyle: 'short' }),
}}
/>
</small>
</div>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Form } from '@edx/paragon';
import { Form } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import './bulkEmailRecepient.scss';
@@ -14,7 +14,13 @@ const DEFAULT_GROUPS = {
};
export default function BulkEmailRecipient(props) {
const { handleCheckboxes, selectedGroups, additionalCohorts } = props;
const {
handleCheckboxes,
selectedGroups,
additionalCohorts,
courseModes,
} = props;
const hasCourseModes = courseModes && courseModes.length > 1;
return (
<Form.Group>
<Form.Label>
@@ -50,18 +56,24 @@ export default function BulkEmailRecipient(props) {
description="A selectable choice from a list of potential email recipients"
/>
</Form.Checkbox>
<Form.Checkbox
key="track:verified"
value="track:verified"
disabled={selectedGroups.find((group) => group === DEFAULT_GROUPS.ALL_LEARNERS)}
className="col col-lg-4 col-sm-6 col-12"
>
<FormattedMessage
id="bulk.email.form.recipients.verified"
defaultMessage="Learners in the verified certificate track"
description="A selectable choice from a list of potential email recipients"
/>
</Form.Checkbox>
{
// additional modes
hasCourseModes
&& courseModes.map((courseMode) => (
<Form.Checkbox
key={`track:${courseMode.slug}`}
value={`track:${courseMode.slug}`}
disabled={selectedGroups.find((group) => group === DEFAULT_GROUPS.ALL_LEARNERS)}
className="col col-lg-4 col-sm-6 col-12"
>
<FormattedMessage
id="bulk.email.form.mode.label"
defaultMessage="Learners in the {courseModeName} Track"
values={{ courseModeName: courseMode.name }}
/>
</Form.Checkbox>
))
}
{
// additional cohorts
additionalCohorts
@@ -80,18 +92,6 @@ export default function BulkEmailRecipient(props) {
</Form.Checkbox>
))
}
<Form.Checkbox
key="track:audit"
value="track:audit"
disabled={selectedGroups.find((group) => group === DEFAULT_GROUPS.ALL_LEARNERS)}
className="col col-lg-4 col-sm-6 col-12"
>
<FormattedMessage
id="bulk.email.form.recipients.audit"
defaultMessage="Learners in the audit track"
description="A selectable choice from a list of potential email recipients"
/>
</Form.Checkbox>
<Form.Checkbox
key="learners"
value="learners"
@@ -127,4 +127,10 @@ BulkEmailRecipient.propTypes = {
handleCheckboxes: PropTypes.func.isRequired,
isValid: PropTypes.bool,
additionalCohorts: PropTypes.arrayOf(PropTypes.string),
courseModes: PropTypes.arrayOf(
PropTypes.shape({
slug: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}),
).isRequired,
};

View File

@@ -0,0 +1,22 @@
import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
/**
* Generates an array of course mode objects using Rosie Factory.
* @returns {Array<Object>} An array of course mode objects with attributes 'slug' and 'name'.
*/
const courseModeFactory = () => {
const AuditModeFactory = Factory.define('AuditModeFactory')
.attr('slug', 'audit')
.attr('name', 'Audit');
const VerifiedModeFactory = Factory.define('VerifiedModeFactory')
.attr('slug', 'verified')
.attr('name', 'Verified Certificate');
return [
AuditModeFactory.build(),
VerifiedModeFactory.build(),
];
};
export default courseModeFactory;

View File

@@ -41,6 +41,11 @@ const messages = defineMessages({
defaultMessage: 'Subject',
description: 'Email subject line input label. Meant to have colon or equivilant punctuation.',
},
bulkEmailFormSubjectTip: {
id: 'bulk.email.form.subject.tip',
defaultMessage: '(Maximum 128 characters)',
description: 'Default Subject tip',
},
bulkEmailFormSubjectError: {
id: 'bulk.email.form.subject.error',
defaultMessage: 'A subject is required',

View File

@@ -12,6 +12,7 @@ import * as bulkEmailFormApi from '../data/api';
import { BulkEmailContext, BulkEmailProvider } from '../../bulk-email-context';
import { formatDate } from '../../../../utils/formatDateAndTime';
import cohortFactory from '../data/__factories__/bulkEmailFormCohort.factory';
import courseModeFactory from '../data/__factories__/bulkEmailFormCourseMode.factory';
jest.mock('../../text-editor/TextEditor');
@@ -20,12 +21,17 @@ const dispatchMock = jest.fn();
const tomorrow = new Date();
tomorrow.setDate(new Date().getDate() + 1);
const courseMode = courseModeFactory();
function renderBulkEmailForm() {
const { cohorts } = cohortFactory.build();
return (
<BulkEmailProvider>
<BulkEmailForm courseId="test" cohorts={cohorts} />
<BulkEmailForm
courseId="test"
cohorts={cohorts}
courseModes={courseMode}
/>
</BulkEmailProvider>
);
}
@@ -33,7 +39,7 @@ function renderBulkEmailForm() {
function renderBulkEmailFormContext(value) {
return (
<BulkEmailContext.Provider value={[value, dispatchMock]}>
<BulkEmailForm courseId="test" />
<BulkEmailForm courseId="test" courseModes={courseMode} />
</BulkEmailContext.Provider>
);
}
@@ -96,8 +102,8 @@ describe('bulk-email-form', () => {
test('Checking "All Learners" disables each learner group', async () => {
render(renderBulkEmailForm());
fireEvent.click(screen.getByRole('checkbox', { name: 'All Learners' }));
const verifiedLearners = screen.getByRole('checkbox', { name: 'Learners in the verified certificate track' });
const auditLearners = screen.getByRole('checkbox', { name: 'Learners in the audit track' });
const verifiedLearners = screen.getByRole('checkbox', { name: 'Learners in the Verified Certificate Track' });
const auditLearners = screen.getByRole('checkbox', { name: 'Learners in the Audit Track' });
const { cohorts } = cohortFactory.build();
cohorts.forEach(cohort => expect(screen.getByRole('checkbox', { name: `Cohort: ${cohort}` })).toBeDisabled());
expect(verifiedLearners).toBeDisabled();

View File

@@ -1,20 +1,21 @@
/* eslint-disable react/no-unstable-nested-components */
import React, { useState } from 'react';
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,
} from '@edx/paragon';
import { SpinnerSimple } from '@edx/paragon/icons';
} from '@openedx/paragon';
import { SpinnerSimple } from '@openedx/paragon/icons';
import messages from './messages';
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);
@@ -51,17 +52,15 @@ function BulkEmailContentHistory({ intl }) {
* up a level (the `subject` field). We also convert the `sent_to` data to be a String rather than an array to fix a
* display bug in the table.
*/
function transformDataForTable() {
let tableData = [];
if (emailHistoryData) {
tableData = emailHistoryData.map((item) => ({
...item,
subject: item.email.subject,
sent_to: item.sent_to.join(', '),
}));
}
return tableData;
}
const transformDataForTable = useMemo(() => {
const tableData = emailHistoryData?.map((item) => ({
...item,
subject: item.email.subject,
sent_to: item.sent_to.join(', '),
created: new Date(item.created).toLocaleString(),
}));
return tableData || [];
}, [emailHistoryData]);
/**
* This function is responsible for setting the current `messageContent` state data. This will be the contents of a
@@ -102,7 +101,7 @@ function BulkEmailContentHistory({ intl }) {
* contents of a previously sent message.
*/
const additionalColumns = () => {
const tableData = transformDataForTable();
const tableData = transformDataForTable;
return [
{
@@ -139,7 +138,7 @@ function BulkEmailContentHistory({ intl }) {
{showHistoricalEmailContentTable ? (
<BulkEmailTaskManagerTable
errorRetrievingData={errorRetrievingData}
tableData={transformDataForTable()}
tableData={transformDataForTable}
tableDescription={intl.formatMessage(messages.emailHistoryTableViewMessageInstructions)}
alertWarningMessage={intl.formatMessage(messages.noEmailData)}
alertErrorMessage={intl.formatMessage(messages.errorFetchingEmailHistoryData)}
@@ -156,7 +155,6 @@ function BulkEmailContentHistory({ intl }) {
}
BulkEmailContentHistory.propTypes = {
intl: intlShape.isRequired,
row: PropTypes.shape({
index: PropTypes.number,
}),
@@ -166,4 +164,4 @@ BulkEmailContentHistory.defaultProps = {
row: {},
};
export default injectIntl(BulkEmailContentHistory);
export default BulkEmailContentHistory;

View File

@@ -1,4 +1,4 @@
import { Alert, DataTable } from '@edx/paragon';
import { Alert, DataTable } from '@openedx/paragon';
import PropTypes from 'prop-types';
import React from 'react';

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

@@ -2,8 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { Hyperlink, Alert } from '@edx/paragon';
import { WarningFilled } from '@edx/paragon/icons';
import { Hyperlink, Alert } from '@openedx/paragon';
import { WarningFilled } from '@openedx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
export default function BulkEmailPendingTasksAlert(props) {

View File

@@ -1,9 +1,9 @@
import React, { useState } from 'react';
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 '@edx/paragon';
import { SpinnerSimple } from '@edx/paragon/icons';
import { Icon, Collapsible } from '@openedx/paragon';
import { SpinnerSimple } from '@openedx/paragon/icons';
import { getEmailTaskHistory } from './data/api';
import messages from './messages';
@@ -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([]);
@@ -41,6 +42,14 @@ function BulkEmailTaskHistory({ intl }) {
setShowHistoricalTaskContentTable(true);
}
const transformDataForTable = useMemo(() => {
const tableData = emailTaskHistoryData?.map((item) => ({
...item,
created: new Date(item.created).toLocaleString(),
}));
return tableData || [];
}, [emailTaskHistoryData]);
const tableColumns = [
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskType)}`,
@@ -95,7 +104,7 @@ function BulkEmailTaskHistory({ intl }) {
{showHistoricalTaskContentTable ? (
<BulkEmailTaskManagerTable
errorRetrievingData={errorRetrievingData}
tableData={emailTaskHistoryData}
tableData={transformDataForTable}
alertWarningMessage={intl.formatMessage(messages.noTaskHistoryData)}
alertErrorMessage={intl.formatMessage(messages.errorFetchingTaskHistoryData)}
columns={tableColumns}
@@ -109,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 '@edx/paragon';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { ActionRow, Button, ModalDialog } from '@openedx/paragon';
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,13 +4,13 @@
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 '@edx/paragon';
} from '@openedx/paragon';
import {
Delete, Info, Visibility, Edit,
} from '@edx/paragon/icons';
} from '@openedx/paragon/icons';
import { useParams } from 'react-router-dom';
import { BulkEmailContext } from '../../bulk-email-context';
import { deleteScheduledEmailThunk, getScheduledBulkEmailThunk } from './data/thunks';
@@ -26,12 +26,14 @@ function flattenScheduledEmailsArray(emails) {
emailId: email.courseEmail.id,
task: email.task,
taskDue: new Date(email.taskDue).toLocaleString(),
taskDueUTC: email.taskDue,
...email.courseEmail,
targets: email.courseEmail.targets.join(', '),
}));
}
function BulkEmailScheduledEmailsTable({ intl }) {
function BulkEmailScheduledEmailsTable() {
const intl = useIntl();
const { courseId } = useParams();
const [{ scheduledEmailsTable }, dispatch] = useContext(BulkEmailContext);
const [tableData, setTableData] = useState([]);
@@ -91,10 +93,10 @@ function BulkEmailScheduledEmailsTable({ intl }) {
const handleEditEmail = (row) => {
const {
original: {
htmlMessage: emailBody, subject: emailSubject, taskDue, targets, schedulingId, emailId,
htmlMessage: emailBody, subject: emailSubject, taskDueUTC, targets, schedulingId, emailId,
},
} = row;
const dateTime = new Date(taskDue);
const dateTime = new Date(taskDueUTC);
const emailRecipients = targets.replaceAll('-', ':').split(', ');
const scheduleDate = formatDate(dateTime);
const scheduleTime = formatTime(dateTime);
@@ -195,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,105 +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];
expect(await screen.findByText(email.created)).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();
expect(await screen.findAllByText(email.created)).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

@@ -61,7 +61,7 @@ describe('BulkEmailPendingTasks component', () => {
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 of 1.')).toBeTruthy();
expect(await screen.findAllByText('Showing 1 - 1 of 1.')).toBeTruthy();
// verification of table contents
const { tasks } = pendingInstructorTaskData;

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,71 +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 of 1.')).toBeTruthy();
// verification of row contents
const { tasks } = taskHistoryData;
const task = tasks[0];
expect(await screen.findByText(task.created)).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 '@edx/paragon';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { ActionRow, AlertModal, Button } from '@openedx/paragon';
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

@@ -18,7 +18,7 @@ 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 contentCss from 'tinymce/skins/content/default/content.css?raw';
export default function TextEditor(props) {
const {

View File

@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button, Icon } from '@edx/paragon';
import { ArrowBack } from '@edx/paragon/icons';
import { Button, Icon } from '@openedx/paragon';
import { ArrowBack } from '@openedx/paragon/icons';
export default function BackToInstructor(props) {
const { courseId } = props;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Nav } from '@edx/paragon';
import { Nav } from '@openedx/paragon';
export default function NavigationTabs(props) {
const { tabData } = props;

View File

@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
import { useParams } from 'react-router-dom';
import { LearningHeader as Header } from '@edx/frontend-component-header';
import Footer from '@edx/frontend-component-footer';
import { Spinner } from '@edx/paragon';
import { FooterSlot } from '@edx/frontend-component-footer';
import { Spinner } from '@openedx/paragon';
import { getCohorts, getCourseHomeCourseMetadata } from './data/api';
@@ -39,7 +39,7 @@ export default function PageContainer(props) {
}
const {
org, number, title, tabs, originalUserIsStaff,
org, number, title, tabs, originalUserIsStaff, courseModes,
} = metadataResponse;
const { cohorts } = cohortsResponse;
@@ -48,6 +48,7 @@ export default function PageContainer(props) {
number,
title,
originalUserIsStaff,
courseModes,
tabs: [...tabs],
cohorts: cohorts.map(({ name }) => name),
});
@@ -66,10 +67,12 @@ export default function PageContainer(props) {
courseNumber={courseMetadata.number}
courseTitle={courseMetadata.title}
/>
<main>
{children}
</main>
<Footer />
<div className="pb-3 container">
<main id="main-content">
{children}
</main>
</div>
<FooterSlot />
</>
</CourseMetadataContext.Provider>
);

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

@@ -1,41 +1 @@
import { messages as footerMessages } from '@edx/frontend-component-footer';
import { messages as headerMessages } from '@edx/frontend-component-header';
import { messages as paragonMessages } from '@edx/paragon';
import arMessages from './messages/ar.json';
import caMessages from './messages/ca.json';
// no need to import en messages-- they are in the defaultMessage field
import es419Messages from './messages/es_419.json';
import frMessages from './messages/fr.json';
import zhcnMessages from './messages/zh_CN.json';
import heMessages from './messages/he.json';
import idMessages from './messages/id.json';
import kokrMessages from './messages/ko_kr.json';
import plMessages from './messages/pl.json';
import ptbrMessages from './messages/pt_br.json';
import ruMessages from './messages/ru.json';
import thMessages from './messages/th.json';
import ukMessages from './messages/uk.json';
const appMessages = {
ar: arMessages,
'es-419': es419Messages,
fr: frMessages,
'zh-cn': zhcnMessages,
ca: caMessages,
he: heMessages,
id: idMessages,
'ko-kr': kokrMessages,
pl: plMessages,
'pt-br': ptbrMessages,
ru: ruMessages,
th: thMessages,
uk: ukMessages,
};
export default [
headerMessages,
footerMessages,
paragonMessages,
appMessages,
];
export default [];

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1 +0,0 @@
{}

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,12 +17,14 @@ 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>
<div className="pb-3 container">
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"
@@ -31,17 +34,22 @@ subscribe(APP_READY, () => {
<BulkEmailTool />
</PageContainer>
</AuthenticatedPageRoute>
)}
)}
/>
</Routes>
</div>
</AppProvider>,
document.getElementById('root'),
</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 "~@edx/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

@@ -0,0 +1,53 @@
# 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-component-footer` repository](https://github.com/openedx/frontend-component-footer/).
## Example
The following `env.config.jsx` will replace the default footer.
![Screenshot of Default Footer](./images/default_footer.png)
with a simple custom footer
![Screenshot of Custom Footer](./images/custom_footer.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.footer.v1': {
plugins: [
{
// Hide the default footer
op: PLUGIN_OPERATIONS.Hide,
widgetId: 'default_contents',
},
{
// Insert a custom footer
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_footer',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🦶</h1>
),
},
},
]
}
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,3 @@
# `frontend-app-communications` Plugin Slots
* [`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', {
@@ -29,6 +30,8 @@ Object.defineProperty(window, 'matchMedia', {
})),
});
global.Date.prototype.toLocaleDateString = jest.fn();
export function initializeMockApp() {
mergeConfig({
// MICROBA-1505: Remove this when we remove the flag from config
@@ -55,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

@@ -1,4 +1,4 @@
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');
const config = createConfig('webpack-dev');
@@ -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

@@ -1,4 +1,4 @@
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');
const config = createConfig('webpack-prod');
@@ -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;