Compare commits
6 Commits
master
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca7bc7d359 | ||
|
|
b41a336e11 | ||
|
|
b4032215c6 | ||
|
|
b20eb50699 | ||
|
|
5c021cdc80 | ||
|
|
fa826fe687 |
2
.env
2
.env
@@ -21,5 +21,3 @@ USER_INFO_COOKIE_NAME=''
|
|||||||
SCHEDULE_EMAIL_SECTION=''
|
SCHEDULE_EMAIL_SECTION=''
|
||||||
APP_ID=''
|
APP_ID=''
|
||||||
MFE_CONFIG_API_URL=''
|
MFE_CONFIG_API_URL=''
|
||||||
# Fallback in local style files
|
|
||||||
PARAGON_THEME_URLS={}
|
|
||||||
|
|||||||
@@ -22,5 +22,3 @@ USER_INFO_COOKIE_NAME='edx-user-info'
|
|||||||
SCHEDULE_EMAIL_SECTION='true'
|
SCHEDULE_EMAIL_SECTION='true'
|
||||||
APP_ID=''
|
APP_ID=''
|
||||||
MFE_CONFIG_API_URL=''
|
MFE_CONFIG_API_URL=''
|
||||||
# Fallback in local style files
|
|
||||||
PARAGON_THEME_URLS={}
|
|
||||||
|
|||||||
@@ -20,4 +20,3 @@ USER_INFO_COOKIE_NAME='edx-user-info'
|
|||||||
SCHEDULE_EMAIL_SECTION='true'
|
SCHEDULE_EMAIL_SECTION='true'
|
||||||
APP_ID=''
|
APP_ID=''
|
||||||
MFE_CONFIG_API_URL=''
|
MFE_CONFIG_API_URL=''
|
||||||
PARAGON_THEME_URLS={}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable import/no-extraneous-dependencies */
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
|
||||||
const { createConfig } = require('@openedx/frontend-build');
|
const { createConfig } = require('@edx/frontend-build');
|
||||||
|
|
||||||
module.exports = createConfig('eslint', {
|
module.exports = createConfig('eslint', {
|
||||||
rules: {
|
rules: {
|
||||||
|
|||||||
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@@ -18,9 +18,9 @@ jobs:
|
|||||||
- name: Setup Nodejs Env
|
- name: Setup Nodejs Env
|
||||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||||
- name: Setup Nodejs
|
- name: Setup Nodejs
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version: ${{ env.NODE_VER }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Validate package-lock.json changes
|
- name: Validate package-lock.json changes
|
||||||
@@ -34,7 +34,4 @@ jobs:
|
|||||||
- name: i18n_extract
|
- name: i18n_extract
|
||||||
run: npm run i18n_extract
|
run: npm run i18n_extract
|
||||||
- name: Coverage
|
- name: Coverage
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
fail_ci_if_error: true
|
|
||||||
|
|||||||
12
.github/workflows/update-browserslist-db.yml
vendored
12
.github/workflows/update-browserslist-db.yml
vendored
@@ -1,12 +0,0 @@
|
|||||||
name: Update Browserslist DB
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * 1'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-browserslist:
|
|
||||||
uses: openedx/.github/.github/workflows/update-browserslist-db.yml@master
|
|
||||||
|
|
||||||
secrets:
|
|
||||||
requirements_bot_github_token: ${{ secrets.requirements_bot_github_token }}
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,7 +5,6 @@ node_modules
|
|||||||
npm-debug.log
|
npm-debug.log
|
||||||
coverage
|
coverage
|
||||||
module.config.js
|
module.config.js
|
||||||
env.config.*
|
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
src/i18n/transifex_input.json
|
src/i18n/transifex_input.json
|
||||||
@@ -18,4 +17,3 @@ temp/babel-plugin-react-intl
|
|||||||
*~
|
*~
|
||||||
/temp
|
/temp
|
||||||
/.vscode
|
/.vscode
|
||||||
src/i18n/messages/
|
|
||||||
|
|||||||
8
.tx/config
Normal file
8
.tx/config
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[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
|
||||||
32
Makefile
32
Makefile
@@ -1,17 +1,19 @@
|
|||||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
export TRANSIFEX_RESOURCE = frontend-app-communications
|
||||||
|
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||||
|
|
||||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||||
i18n = ./src/i18n
|
i18n = ./src/i18n
|
||||||
transifex_input = $(i18n)/transifex_input.json
|
transifex_input = $(i18n)/transifex_input.json
|
||||||
|
|
||||||
# This directory must match .babelrc .
|
# This directory must match .babelrc .
|
||||||
transifex_temp = ./temp/babel-plugin-formatjs
|
transifex_temp = ./temp/babel-plugin-react-intl
|
||||||
|
|
||||||
precommit:
|
precommit:
|
||||||
npm run lint
|
npm run lint
|
||||||
npm audit
|
npm audit
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
npm ci
|
npm install
|
||||||
|
|
||||||
i18n.extract:
|
i18n.extract:
|
||||||
# Pulling display strings from .jsx files into .json files...
|
# Pulling display strings from .jsx files into .json files...
|
||||||
@@ -29,18 +31,20 @@ detect_changed_source_translations:
|
|||||||
# Checking for changed translations...
|
# Checking for changed translations...
|
||||||
git diff --exit-code $(i18n)
|
git diff --exit-code $(i18n)
|
||||||
|
|
||||||
pull_translations:
|
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||||
rm -rf src/i18n/messages
|
push_translations:
|
||||||
mkdir src/i18n/messages
|
# Pushing strings to Transifex...
|
||||||
cd src/i18n/messages \
|
tx push -s
|
||||||
&& atlas pull $(ATLAS_OPTIONS) \
|
# Fetching hashes from Transifex...
|
||||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
# Writing out comments to file...
|
||||||
translations/paragon/src/i18n/messages:paragon \
|
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
# Pushing comments to Transifex...
|
||||||
translations/frontend-app-communications/src/i18n/messages:frontend-app-communications
|
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||||
|
|
||||||
$(intl_imports) frontend-component-header frontend-component-footer paragon frontend-platform frontend-app-communications
|
# Pulls translations from Transifex.
|
||||||
|
pull_translations:
|
||||||
|
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||||
|
|
||||||
# This target is used by Travis.
|
# This target is used by Travis.
|
||||||
validate-no-uncommitted-package-lock-changes:
|
validate-no-uncommitted-package-lock-changes:
|
||||||
|
|||||||
179
README.rst
179
README.rst
@@ -1,72 +1,55 @@
|
|||||||
|
|Codecov| |license|
|
||||||
|
|
||||||
|
.. |codecov| image:: https://codecov.io/gh/edx/frontend-app-learning/branch/master/graph/badge.svg?token=3z7XvuzTq3
|
||||||
|
:target: https://codecov.io/gh/edx/frontend-app-communications
|
||||||
|
.. |license| image:: https://img.shields.io/badge/license-AGPL-informational
|
||||||
|
:target: https://github.com/edx/frontend-app-account/blob/master/LICENSE
|
||||||
|
|
||||||
frontend-app-communications
|
frontend-app-communications
|
||||||
###########################
|
==============================
|
||||||
|
|
||||||
|license-badge| |status-badge| |ci-badge| |codecov-badge|
|
Please tag **edx-aperture** on any PRs or issues. Thanks!
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
|
A tool used by course teams to communicate with thier learners. The interface for anything related to instructor to learner communications. Instructor bulk email, for example.
|
||||||
|
|
||||||
|
|
||||||
Purpose
|
Getting started
|
||||||
********
|
------------
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
Getting Started
|
1. Clone the repo into your usual workspace
|
||||||
***************
|
|
||||||
|
|
||||||
Prerequisites
|
.. code-block::
|
||||||
=============
|
|
||||||
|
|
||||||
`Tutor`_ is currently recommended as a development environment for your
|
mkdir -p ~/workspace/
|
||||||
new MFE. Please refer
|
cd ~/workspace/
|
||||||
to the `relevant tutor-mfe documentation`_ to get started using it.
|
git clone https://github.com/edx/frontend-app-communications.git
|
||||||
|
|
||||||
.. _Tutor: https://github.com/overhangio/tutor
|
2. Install frontend dependencies
|
||||||
|
|
||||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
|
.. code-block::
|
||||||
|
|
||||||
|
npm i
|
||||||
|
|
||||||
Cloning and Startup
|
3. Start the devserver. The app will be running at ``localhost:1984``, or whatever port you change it too.
|
||||||
===================
|
|
||||||
|
|
||||||
1. Clone your new repo:
|
.. code-block::
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
git clone https://github.com/edx/frontend-app-communications.git
|
|
||||||
|
|
||||||
2. Use the version of Node specified in the ``.nvmrc`` file.
|
|
||||||
|
|
||||||
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>`_.
|
|
||||||
|
|
||||||
3. Install npm dependencies:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
cd frontend-app-communications && npm install
|
|
||||||
|
|
||||||
4. Update the application port to use for local development:
|
|
||||||
|
|
||||||
The default port is 1984. If this does not work for you, update the line
|
|
||||||
``PORT=1984`` to your port in all ``.env.*`` files
|
|
||||||
|
|
||||||
5. Start the devserver. The app will be running at ``localhost:1984``, or whatever port you change it too.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
npm start
|
npm start
|
||||||
|
|
||||||
|
|
||||||
Environment Variables/Setup Notes
|
Environment Variables/Setup Notes
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
If you wish to add new environment variables for local testing, they should be listed in 2 places:
|
If you wish to add new environment varibles for local testing, they should be listed in 2 places:
|
||||||
|
|
||||||
1. In ``.env.development``
|
1. In ``.env.development``
|
||||||
2. Added to the ``mergeConfig`` found in ``src/index.jsx``
|
2. Added to the ``mergeConfig`` found in ``src/index.jsx``
|
||||||
|
|
||||||
.. code-block:: jsx
|
.. code-block::
|
||||||
|
|
||||||
initialize({
|
initialize({
|
||||||
config: () => {
|
config: () => {
|
||||||
@@ -75,108 +58,10 @@ If you wish to add new environment variables for local testing, they should be l
|
|||||||
}, 'CommuncationsAppConfig');
|
}, 'CommuncationsAppConfig');
|
||||||
|
|
||||||
Running Tests
|
Running Tests
|
||||||
-------------
|
---------------------------
|
||||||
|
|
||||||
Tests use `jest` and `react-test-library`. To run all the tests for this repo:
|
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**
|
|
||||||
|
|
||||||
The production build is created with ``npm run build``.
|
|
||||||
|
|
||||||
Internationalization
|
|
||||||
====================
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Getting Help
|
|
||||||
************
|
|
||||||
|
|
||||||
If you're having trouble, we have discussion forums at
|
|
||||||
https://discuss.openedx.org where you can connect with others in the community.
|
|
||||||
|
|
||||||
Our real-time conversations are on Slack. You can request a `Slack
|
|
||||||
invitation`_, then join our `community Slack workspace`_. Because this is a
|
|
||||||
frontend repository, the best place to discuss it would be in the `#wg-frontend
|
|
||||||
channel`_.
|
|
||||||
|
|
||||||
For anything non-trivial, the best path is to open an issue in this repository
|
|
||||||
with as many details about the issue you are facing as you can provide.
|
|
||||||
|
|
||||||
https://github.com/openedx/frontend-app-communications/issues
|
|
||||||
|
|
||||||
For more information about these options, see the `Getting Help`_ page.
|
|
||||||
|
|
||||||
.. _Slack invitation: https://openedx.org/slack
|
|
||||||
.. _community Slack workspace: https://openedx.slack.com/
|
|
||||||
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
|
|
||||||
.. _Getting Help: https://openedx.org/community/connect
|
|
||||||
|
|
||||||
License
|
|
||||||
*******
|
|
||||||
|
|
||||||
The code in this repository is licensed under the AGPLv3 unless otherwise
|
|
||||||
noted.
|
|
||||||
|
|
||||||
Please see `LICENSE <LICENSE>`_ for details.
|
|
||||||
|
|
||||||
Contributing
|
|
||||||
************
|
|
||||||
|
|
||||||
Contributions are very welcome. Please read `How To Contribute`_ for details.
|
|
||||||
|
|
||||||
.. _How To Contribute: https://openedx.org/r/how-to-contribute
|
|
||||||
|
|
||||||
This project is currently accepting all types of contributions, bug fixes,
|
|
||||||
security fixes, maintenance work, or new features. However, please make sure
|
|
||||||
to have a discussion about your new feature idea with the maintainers prior to
|
|
||||||
beginning development to maximize the chances of your change being accepted.
|
|
||||||
You can start a conversation by creating a new issue on this repo summarizing
|
|
||||||
your idea.
|
|
||||||
|
|
||||||
The Open edX Code of Conduct
|
|
||||||
****************************
|
|
||||||
|
|
||||||
All community members are expected to follow the `Open edX Code of Conduct`_.
|
|
||||||
|
|
||||||
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
|
|
||||||
|
|
||||||
People
|
|
||||||
******
|
|
||||||
|
|
||||||
The assigned maintainers for this component and other project details may be
|
|
||||||
found in `Backstage`_. Backstage pulls this data from the ``catalog-info.yaml``
|
|
||||||
file in this repo.
|
|
||||||
|
|
||||||
.. _Backstage: https://open-edx-backstage.herokuapp.com/catalog/default/component/frontend-app-communications
|
|
||||||
|
|
||||||
Reporting Security Issues
|
|
||||||
*************************
|
|
||||||
|
|
||||||
Please do not report security issues in public, and email security@openedx.org instead.
|
|
||||||
|
|
||||||
.. |license-badge| image:: https://img.shields.io/github/license/openedx/frontend-app-communications.svg
|
|
||||||
:target: https://github.com/openedx/frontend-app-communications/blob/master/LICENSE
|
|
||||||
:alt: License
|
|
||||||
|
|
||||||
.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen
|
|
||||||
|
|
||||||
.. |ci-badge| image:: https://github.com/openedx/frontend-app-communications/actions/workflows/ci.yml/badge.svg
|
|
||||||
:target: https://github.com/openedx/frontend-app-communications/actions/workflows/ci.yml
|
|
||||||
:alt: Continuous Integration
|
|
||||||
|
|
||||||
.. |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
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
# This file records information about this repo. Its use is described in OEP-55:
|
|
||||||
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
|
|
||||||
|
|
||||||
apiVersion: backstage.io/v1alpha1
|
|
||||||
kind: Component
|
|
||||||
metadata:
|
|
||||||
name: "frontend-app-communications"
|
|
||||||
description: "A tool used by course teams to communicate with their learners."
|
|
||||||
links:
|
|
||||||
- url: "https://github.com/openedx/frontend-app-communications/blob/master/README.rst"
|
|
||||||
title: "README"
|
|
||||||
icon: "Article"
|
|
||||||
annotations:
|
|
||||||
openedx.org/arch-interest-groups: ""
|
|
||||||
openedx.org/release: "master"
|
|
||||||
spec:
|
|
||||||
owner: group:committers-frontend
|
|
||||||
type: "service"
|
|
||||||
lifecycle: "production"
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const { createConfig } = require('@openedx/frontend-build');
|
const { createConfig } = require('@edx/frontend-build');
|
||||||
|
|
||||||
module.exports = createConfig('jest', {
|
module.exports = createConfig('jest', {
|
||||||
// setupFilesAfterEnv is used after the jest environment has been loaded. In general this is what you want.
|
// setupFilesAfterEnv is used after the jest environment has been loaded. In general this is what you want.
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ module.exports = {
|
|||||||
**********************************************************************************************/
|
**********************************************************************************************/
|
||||||
|
|
||||||
// { moduleName: '@edx/brand', dir: '../brand-openedx' }, // replace with your brand checkout
|
// { moduleName: '@edx/brand', dir: '../brand-openedx' }, // replace with your brand checkout
|
||||||
// { moduleName: '@openedx/paragon/scss/core', dir: '../paragon', dist: 'scss/core' },
|
// { moduleName: '@edx/paragon/scss/core', dir: '../paragon', dist: 'scss/core' },
|
||||||
// { moduleName: '@openedx/paragon/icons', dir: '../paragon', dist: 'icons' },
|
// { moduleName: '@edx/paragon/icons', dir: '../paragon', dist: 'icons' },
|
||||||
// { moduleName: '@openedx/paragon', dir: '../paragon', dist: 'dist' },
|
// { moduleName: '@edx/paragon', dir: '../paragon', dist: 'dist' },
|
||||||
// { moduleName: '@edx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
|
// { moduleName: '@edx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
9
openedx.yaml
Normal file
9
openedx.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# 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
|
||||||
27925
package-lock.json
generated
27925
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
@@ -11,14 +11,18 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "fedx-scripts webpack",
|
"build": "fedx-scripts webpack",
|
||||||
"i18n_extract": "fedx-scripts formatjs extract",
|
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx src/",
|
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx src/",
|
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
|
||||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||||
"start": "fedx-scripts webpack-dev-server --progress",
|
"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"
|
"test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests"
|
||||||
},
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "npm run lint"
|
||||||
|
}
|
||||||
|
},
|
||||||
"author": "edX",
|
"author": "edX",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"homepage": "https://github.com/edx/frontend-app-communications#readme",
|
"homepage": "https://github.com/edx/frontend-app-communications#readme",
|
||||||
@@ -29,19 +33,17 @@
|
|||||||
"url": "https://github.com/edx/frontend-app-communications/issues"
|
"url": "https://github.com/edx/frontend-app-communications/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||||
"@edx/frontend-component-footer": "^14.6.0",
|
"@edx/frontend-component-footer": "^12.0.0",
|
||||||
"@edx/frontend-component-header": "^6.6.1",
|
"@edx/frontend-component-header": "^4.0.0",
|
||||||
"@edx/frontend-platform": "^8.3.7",
|
"@edx/frontend-platform": "^4.0.1",
|
||||||
"@edx/openedx-atlas": "^0.6.0",
|
"@edx/paragon": "^20.20.0",
|
||||||
"@edx/tinymce-language-selector": "1.1.0",
|
"@edx/tinymce-language-selector": "1.1.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||||
"@fortawesome/react-fontawesome": "0.2.0",
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
"@openedx/frontend-plugin-framework": "^1.6.0",
|
|
||||||
"@openedx/paragon": "^23.3.0",
|
|
||||||
"@tinymce/tinymce-react": "3.14.0",
|
"@tinymce/tinymce-react": "3.14.0",
|
||||||
"axios": "0.27.2",
|
"axios": "0.27.2",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.3.2",
|
||||||
@@ -49,25 +51,26 @@
|
|||||||
"jquery": "3.6.1",
|
"jquery": "3.6.1",
|
||||||
"popper.js": "1.16.1",
|
"popper.js": "1.16.1",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"react": "^18.3.1",
|
"react": "16.14.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "16.14.0",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-redux": "7.2.9",
|
"react-redux": "7.2.9",
|
||||||
"react-router": "6.15.0",
|
"react-router": "5.3.4",
|
||||||
"react-router-dom": "6.15.0",
|
"react-router-dom": "5.3.4",
|
||||||
"redux": "4.2.0",
|
"redux": "4.2.0",
|
||||||
"regenerator-runtime": "0.13.11",
|
"regenerator-runtime": "0.13.11",
|
||||||
"tinymce": "5.10.7"
|
"tinymce": "5.10.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/browserslist-config": "^1.2.0",
|
"@edx/browserslist-config": "^1.2.0",
|
||||||
"@edx/typescript-config": "^1.1.0",
|
"@edx/frontend-build": "^12.7.0",
|
||||||
"@openedx/frontend-build": "^14.6.2",
|
"@edx/reactifex": "^2.1.1",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "5.16.5",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "12.1.5",
|
||||||
"axios-mock-adapter": "1.21.2",
|
"axios-mock-adapter": "1.21.2",
|
||||||
"glob": "7.2.3",
|
"glob": "7.2.3",
|
||||||
"jest": "29.7.0",
|
"husky": "7.0.4",
|
||||||
|
"jest": "27.5.1",
|
||||||
"prettier": "2.8.1",
|
"prettier": "2.8.1",
|
||||||
"rosie": "2.1.0"
|
"rosie": "2.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
<title>Communications | <%= process.env.SITE_NAME %></title>
|
<title>Communications | <%= process.env.SITE_NAME %></title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"automerge": true
|
"automerge": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchPackagePatterns": ["@edx", "@openedx"],
|
"matchPackagePatterns": ["@edx"],
|
||||||
"matchUpdateTypes": ["minor", "patch"],
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
"automerge": true
|
"automerge": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
// 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>
|
|
||||||
`;
|
|
||||||
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||||
import { Container } from '@openedx/paragon';
|
import { Container } from '@edx/paragon';
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
import BulkEmailTaskManager from './bulk-email-task-manager/BulkEmailTaskManager';
|
import BulkEmailTaskManager from './bulk-email-task-manager/BulkEmailTaskManager';
|
||||||
import NavigationTabs from '../navigation-tabs/NavigationTabs';
|
import NavigationTabs from '../navigation-tabs/NavigationTabs';
|
||||||
@@ -24,7 +24,7 @@ export default function BulkEmailTool() {
|
|||||||
<Container size="md">
|
<Container size="md">
|
||||||
<BackToInstructor courseId={courseId} />
|
<BackToInstructor courseId={courseId} />
|
||||||
<div className="row pb-4.5">
|
<div className="row pb-4.5">
|
||||||
<h1 className="text-primary-500">
|
<h1 className="text-primary-500" id="main-content">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="bulk.email.send.email.header"
|
id="bulk.email.send.email.header"
|
||||||
defaultMessage="Send an email"
|
defaultMessage="Send an email"
|
||||||
@@ -33,11 +33,7 @@ export default function BulkEmailTool() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<BulkEmailForm
|
<BulkEmailForm courseId={courseId} cohorts={courseMetadata.cohorts} />
|
||||||
courseId={courseId}
|
|
||||||
cohorts={courseMetadata.cohorts}
|
|
||||||
courseModes={courseMetadata.courseModes}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="row py-5">
|
<div className="row py-5">
|
||||||
<BulkEmailTaskManager courseId={courseId} />
|
<BulkEmailTaskManager courseId={courseId} />
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import PropTypes from 'prop-types';
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Form, Icon, StatefulButton, Toast, useToggle,
|
Form, Icon, StatefulButton, Toast, useToggle,
|
||||||
} from '@openedx/paragon';
|
} from '@edx/paragon';
|
||||||
import {
|
import {
|
||||||
SpinnerSimple, Cancel, Send, Event, Check,
|
SpinnerSimple, Cancel, Send, Event, Check,
|
||||||
} from '@openedx/paragon/icons';
|
} from '@edx/paragon/icons';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import TextEditor from '../text-editor/TextEditor';
|
import TextEditor from '../text-editor/TextEditor';
|
||||||
@@ -47,12 +47,7 @@ const FORM_ACTIONS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function BulkEmailForm(props) {
|
function BulkEmailForm(props) {
|
||||||
const {
|
const { courseId, cohorts, intl } = props;
|
||||||
courseId,
|
|
||||||
cohorts,
|
|
||||||
courseModes,
|
|
||||||
} = props;
|
|
||||||
const intl = useIntl();
|
|
||||||
const [{ editor }, dispatch] = useContext(BulkEmailContext);
|
const [{ editor }, dispatch] = useContext(BulkEmailContext);
|
||||||
const [emailFormStatus, setEmailFormStatus] = useState(FORM_SUBMIT_STATES.DEFAULT);
|
const [emailFormStatus, setEmailFormStatus] = useState(FORM_SUBMIT_STATES.DEFAULT);
|
||||||
const [emailFormValidation, setEmailFormValidation] = useState({
|
const [emailFormValidation, setEmailFormValidation] = useState({
|
||||||
@@ -277,14 +272,10 @@ function BulkEmailForm(props) {
|
|||||||
handleCheckboxes={onRecipientChange}
|
handleCheckboxes={onRecipientChange}
|
||||||
additionalCohorts={cohorts}
|
additionalCohorts={cohorts}
|
||||||
isValid={emailFormValidation.recipients}
|
isValid={emailFormValidation.recipients}
|
||||||
courseModes={courseModes}
|
|
||||||
/>
|
/>
|
||||||
<Form.Group controlId="emailSubject">
|
<Form.Group controlId="emailSubject">
|
||||||
<Form.Label className="h3 text-primary-500">{intl.formatMessage(messages.bulkEmailSubjectLabel)}</Form.Label>
|
<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} maxLength={128} />
|
<Form.Control name="emailSubject" className="w-lg-50" onChange={onFormChange} value={editor.emailSubject} />
|
||||||
<Form.Control.Feedback className="px-3" type="default">
|
|
||||||
{intl.formatMessage(messages.bulkEmailFormSubjectTip)}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
{!emailFormValidation.subject && (
|
{!emailFormValidation.subject && (
|
||||||
<Form.Control.Feedback className="px-3" hasIcon type="invalid">
|
<Form.Control.Feedback className="px-3" hasIcon type="invalid">
|
||||||
{intl.formatMessage(messages.bulkEmailFormSubjectError)}
|
{intl.formatMessage(messages.bulkEmailFormSubjectError)}
|
||||||
@@ -392,13 +383,7 @@ BulkEmailForm.defaultProps = {
|
|||||||
BulkEmailForm.propTypes = {
|
BulkEmailForm.propTypes = {
|
||||||
courseId: PropTypes.string.isRequired,
|
courseId: PropTypes.string.isRequired,
|
||||||
cohorts: PropTypes.arrayOf(PropTypes.string),
|
cohorts: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
intl: intlShape.isRequired,
|
||||||
courseModes: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
slug: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
}),
|
|
||||||
).isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BulkEmailForm;
|
export default injectIntl(BulkEmailForm);
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
import { Form } from '@openedx/paragon';
|
import { Form } from '@edx/paragon';
|
||||||
import useMobileResponsive from '../../../utils/useMobileResponsive';
|
import useMobileResponsive from '../../../utils/useMobileResponsive';
|
||||||
|
|
||||||
function ScheduleEmailForm(props) {
|
function ScheduleEmailForm(props) {
|
||||||
const isMobile = useMobileResponsive();
|
const isMobile = useMobileResponsive();
|
||||||
const { isValid, onDateTimeChange, dateTime } = props;
|
const { isValid, onDateTimeChange, dateTime } = props;
|
||||||
const { date, time } = dateTime;
|
const { date, time } = dateTime;
|
||||||
const descriptionDate = new Date();
|
|
||||||
descriptionDate.setDate(new Date().getDate() + 1);
|
|
||||||
return (
|
return (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<div className={classNames('d-flex', isMobile ? 'flex-column' : 'flex-row', 'my-3')}>
|
<div className={classNames('d-flex', isMobile ? 'flex-column' : 'flex-row', 'my-3')}>
|
||||||
@@ -32,10 +30,7 @@ function ScheduleEmailForm(props) {
|
|||||||
<small className="text-gray-500 x-small">
|
<small className="text-gray-500 x-small">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="bulk.email.form.schedule.date.description"
|
id="bulk.email.form.schedule.date.description"
|
||||||
defaultMessage="Enter a start date, e.g. {date}"
|
defaultMessage="Enter a start date, e.g. 11/27/2023"
|
||||||
values={{
|
|
||||||
date: descriptionDate.toLocaleDateString(),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,10 +52,7 @@ function ScheduleEmailForm(props) {
|
|||||||
<small className="text-gray-500 x-small">
|
<small className="text-gray-500 x-small">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="bulk.email.form.schedule.time.description"
|
id="bulk.email.form.schedule.time.description"
|
||||||
defaultMessage="Enter a start time, e.g. {time}"
|
defaultMessage="Enter a start time, e.g. 09:00 AM"
|
||||||
values={{
|
|
||||||
time: descriptionDate.toLocaleTimeString([], { timeStyle: 'short' }),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Form } from '@openedx/paragon';
|
import { Form } from '@edx/paragon';
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import './bulkEmailRecepient.scss';
|
import './bulkEmailRecepient.scss';
|
||||||
@@ -14,13 +14,7 @@ const DEFAULT_GROUPS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function BulkEmailRecipient(props) {
|
export default function BulkEmailRecipient(props) {
|
||||||
const {
|
const { handleCheckboxes, selectedGroups, additionalCohorts } = props;
|
||||||
handleCheckboxes,
|
|
||||||
selectedGroups,
|
|
||||||
additionalCohorts,
|
|
||||||
courseModes,
|
|
||||||
} = props;
|
|
||||||
const hasCourseModes = courseModes && courseModes.length > 1;
|
|
||||||
return (
|
return (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Label>
|
<Form.Label>
|
||||||
@@ -56,24 +50,18 @@ export default function BulkEmailRecipient(props) {
|
|||||||
description="A selectable choice from a list of potential email recipients"
|
description="A selectable choice from a list of potential email recipients"
|
||||||
/>
|
/>
|
||||||
</Form.Checkbox>
|
</Form.Checkbox>
|
||||||
{
|
<Form.Checkbox
|
||||||
// additional modes
|
key="track:verified"
|
||||||
hasCourseModes
|
value="track:verified"
|
||||||
&& courseModes.map((courseMode) => (
|
disabled={selectedGroups.find((group) => group === DEFAULT_GROUPS.ALL_LEARNERS)}
|
||||||
<Form.Checkbox
|
className="col col-lg-4 col-sm-6 col-12"
|
||||||
key={`track:${courseMode.slug}`}
|
>
|
||||||
value={`track:${courseMode.slug}`}
|
<FormattedMessage
|
||||||
disabled={selectedGroups.find((group) => group === DEFAULT_GROUPS.ALL_LEARNERS)}
|
id="bulk.email.form.recipients.verified"
|
||||||
className="col col-lg-4 col-sm-6 col-12"
|
defaultMessage="Learners in the verified certificate track"
|
||||||
>
|
description="A selectable choice from a list of potential email recipients"
|
||||||
<FormattedMessage
|
/>
|
||||||
id="bulk.email.form.mode.label"
|
</Form.Checkbox>
|
||||||
defaultMessage="Learners in the {courseModeName} Track"
|
|
||||||
values={{ courseModeName: courseMode.name }}
|
|
||||||
/>
|
|
||||||
</Form.Checkbox>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
// additional cohorts
|
// additional cohorts
|
||||||
additionalCohorts
|
additionalCohorts
|
||||||
@@ -92,6 +80,18 @@ export default function BulkEmailRecipient(props) {
|
|||||||
</Form.Checkbox>
|
</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
|
<Form.Checkbox
|
||||||
key="learners"
|
key="learners"
|
||||||
value="learners"
|
value="learners"
|
||||||
@@ -127,10 +127,4 @@ BulkEmailRecipient.propTypes = {
|
|||||||
handleCheckboxes: PropTypes.func.isRequired,
|
handleCheckboxes: PropTypes.func.isRequired,
|
||||||
isValid: PropTypes.bool,
|
isValid: PropTypes.bool,
|
||||||
additionalCohorts: PropTypes.arrayOf(PropTypes.string),
|
additionalCohorts: PropTypes.arrayOf(PropTypes.string),
|
||||||
courseModes: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
slug: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
}),
|
|
||||||
).isRequired,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -41,11 +41,6 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Subject',
|
defaultMessage: 'Subject',
|
||||||
description: 'Email subject line input label. Meant to have colon or equivilant punctuation.',
|
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: {
|
bulkEmailFormSubjectError: {
|
||||||
id: 'bulk.email.form.subject.error',
|
id: 'bulk.email.form.subject.error',
|
||||||
defaultMessage: 'A subject is required',
|
defaultMessage: 'A subject is required',
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import * as bulkEmailFormApi from '../data/api';
|
|||||||
import { BulkEmailContext, BulkEmailProvider } from '../../bulk-email-context';
|
import { BulkEmailContext, BulkEmailProvider } from '../../bulk-email-context';
|
||||||
import { formatDate } from '../../../../utils/formatDateAndTime';
|
import { formatDate } from '../../../../utils/formatDateAndTime';
|
||||||
import cohortFactory from '../data/__factories__/bulkEmailFormCohort.factory';
|
import cohortFactory from '../data/__factories__/bulkEmailFormCohort.factory';
|
||||||
import courseModeFactory from '../data/__factories__/bulkEmailFormCourseMode.factory';
|
|
||||||
|
|
||||||
jest.mock('../../text-editor/TextEditor');
|
jest.mock('../../text-editor/TextEditor');
|
||||||
|
|
||||||
@@ -21,17 +20,12 @@ const dispatchMock = jest.fn();
|
|||||||
|
|
||||||
const tomorrow = new Date();
|
const tomorrow = new Date();
|
||||||
tomorrow.setDate(new Date().getDate() + 1);
|
tomorrow.setDate(new Date().getDate() + 1);
|
||||||
const courseMode = courseModeFactory();
|
|
||||||
|
|
||||||
function renderBulkEmailForm() {
|
function renderBulkEmailForm() {
|
||||||
const { cohorts } = cohortFactory.build();
|
const { cohorts } = cohortFactory.build();
|
||||||
return (
|
return (
|
||||||
<BulkEmailProvider>
|
<BulkEmailProvider>
|
||||||
<BulkEmailForm
|
<BulkEmailForm courseId="test" cohorts={cohorts} />
|
||||||
courseId="test"
|
|
||||||
cohorts={cohorts}
|
|
||||||
courseModes={courseMode}
|
|
||||||
/>
|
|
||||||
</BulkEmailProvider>
|
</BulkEmailProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -39,7 +33,7 @@ function renderBulkEmailForm() {
|
|||||||
function renderBulkEmailFormContext(value) {
|
function renderBulkEmailFormContext(value) {
|
||||||
return (
|
return (
|
||||||
<BulkEmailContext.Provider value={[value, dispatchMock]}>
|
<BulkEmailContext.Provider value={[value, dispatchMock]}>
|
||||||
<BulkEmailForm courseId="test" courseModes={courseMode} />
|
<BulkEmailForm courseId="test" />
|
||||||
</BulkEmailContext.Provider>
|
</BulkEmailContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -102,8 +96,8 @@ describe('bulk-email-form', () => {
|
|||||||
test('Checking "All Learners" disables each learner group', async () => {
|
test('Checking "All Learners" disables each learner group', async () => {
|
||||||
render(renderBulkEmailForm());
|
render(renderBulkEmailForm());
|
||||||
fireEvent.click(screen.getByRole('checkbox', { name: 'All Learners' }));
|
fireEvent.click(screen.getByRole('checkbox', { name: 'All Learners' }));
|
||||||
const verifiedLearners = screen.getByRole('checkbox', { name: 'Learners in the Verified Certificate 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 auditLearners = screen.getByRole('checkbox', { name: 'Learners in the audit track' });
|
||||||
const { cohorts } = cohortFactory.build();
|
const { cohorts } = cohortFactory.build();
|
||||||
cohorts.forEach(cohort => expect(screen.getByRole('checkbox', { name: `Cohort: ${cohort}` })).toBeDisabled());
|
cohorts.forEach(cohort => expect(screen.getByRole('checkbox', { name: `Cohort: ${cohort}` })).toBeDisabled());
|
||||||
expect(verifiedLearners).toBeDisabled();
|
expect(verifiedLearners).toBeDisabled();
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
/* eslint-disable react/no-unstable-nested-components */
|
/* eslint-disable react/no-unstable-nested-components */
|
||||||
|
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button, Collapsible, Icon,
|
Button, Collapsible, Icon,
|
||||||
} from '@openedx/paragon';
|
} from '@edx/paragon';
|
||||||
import { SpinnerSimple } from '@openedx/paragon/icons';
|
import { SpinnerSimple } from '@edx/paragon/icons';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { getSentEmailHistory } from './data/api';
|
import { getSentEmailHistory } from './data/api';
|
||||||
import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
|
import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
|
||||||
import ViewEmailModal from './ViewEmailModal';
|
import ViewEmailModal from './ViewEmailModal';
|
||||||
|
|
||||||
function BulkEmailContentHistory() {
|
function BulkEmailContentHistory({ intl }) {
|
||||||
const intl = useIntl();
|
|
||||||
const { courseId } = useParams();
|
const { courseId } = useParams();
|
||||||
const [emailHistoryData, setEmailHistoryData] = useState();
|
const [emailHistoryData, setEmailHistoryData] = useState();
|
||||||
const [errorRetrievingData, setErrorRetrievingData] = useState(false);
|
const [errorRetrievingData, setErrorRetrievingData] = useState(false);
|
||||||
@@ -52,15 +51,17 @@ function BulkEmailContentHistory() {
|
|||||||
* up a level (the `subject` field). We also convert the `sent_to` data to be a String rather than an array to fix a
|
* 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.
|
* display bug in the table.
|
||||||
*/
|
*/
|
||||||
const transformDataForTable = useMemo(() => {
|
function transformDataForTable() {
|
||||||
const tableData = emailHistoryData?.map((item) => ({
|
let tableData = [];
|
||||||
...item,
|
if (emailHistoryData) {
|
||||||
subject: item.email.subject,
|
tableData = emailHistoryData.map((item) => ({
|
||||||
sent_to: item.sent_to.join(', '),
|
...item,
|
||||||
created: new Date(item.created).toLocaleString(),
|
subject: item.email.subject,
|
||||||
}));
|
sent_to: item.sent_to.join(', '),
|
||||||
return tableData || [];
|
}));
|
||||||
}, [emailHistoryData]);
|
}
|
||||||
|
return tableData;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is responsible for setting the current `messageContent` state data. This will be the contents of a
|
* This function is responsible for setting the current `messageContent` state data. This will be the contents of a
|
||||||
@@ -101,7 +102,7 @@ function BulkEmailContentHistory() {
|
|||||||
* contents of a previously sent message.
|
* contents of a previously sent message.
|
||||||
*/
|
*/
|
||||||
const additionalColumns = () => {
|
const additionalColumns = () => {
|
||||||
const tableData = transformDataForTable;
|
const tableData = transformDataForTable();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -138,7 +139,7 @@ function BulkEmailContentHistory() {
|
|||||||
{showHistoricalEmailContentTable ? (
|
{showHistoricalEmailContentTable ? (
|
||||||
<BulkEmailTaskManagerTable
|
<BulkEmailTaskManagerTable
|
||||||
errorRetrievingData={errorRetrievingData}
|
errorRetrievingData={errorRetrievingData}
|
||||||
tableData={transformDataForTable}
|
tableData={transformDataForTable()}
|
||||||
tableDescription={intl.formatMessage(messages.emailHistoryTableViewMessageInstructions)}
|
tableDescription={intl.formatMessage(messages.emailHistoryTableViewMessageInstructions)}
|
||||||
alertWarningMessage={intl.formatMessage(messages.noEmailData)}
|
alertWarningMessage={intl.formatMessage(messages.noEmailData)}
|
||||||
alertErrorMessage={intl.formatMessage(messages.errorFetchingEmailHistoryData)}
|
alertErrorMessage={intl.formatMessage(messages.errorFetchingEmailHistoryData)}
|
||||||
@@ -155,6 +156,7 @@ function BulkEmailContentHistory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BulkEmailContentHistory.propTypes = {
|
BulkEmailContentHistory.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
row: PropTypes.shape({
|
row: PropTypes.shape({
|
||||||
index: PropTypes.number,
|
index: PropTypes.number,
|
||||||
}),
|
}),
|
||||||
@@ -164,4 +166,4 @@ BulkEmailContentHistory.defaultProps = {
|
|||||||
row: {},
|
row: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BulkEmailContentHistory;
|
export default injectIntl(BulkEmailContentHistory);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Alert, DataTable } from '@openedx/paragon';
|
import { Alert, DataTable } from '@edx/paragon';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { getInstructorTasks } from './data/api';
|
import { getInstructorTasks } from './data/api';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import useInterval from '../../../utils/useInterval';
|
import useInterval from '../../../utils/useInterval';
|
||||||
import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
|
import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
|
||||||
|
|
||||||
function BulkEmailPendingTasks() {
|
function BulkEmailPendingTasks({ intl }) {
|
||||||
const intl = useIntl();
|
|
||||||
const { courseId } = useParams();
|
const { courseId } = useParams();
|
||||||
|
|
||||||
const [instructorTaskData, setInstructorTaskData] = useState();
|
const [instructorTaskData, setInstructorTaskData] = useState();
|
||||||
@@ -90,4 +89,8 @@ function BulkEmailPendingTasks() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BulkEmailPendingTasks;
|
BulkEmailPendingTasks.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(BulkEmailPendingTasks);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { Hyperlink, Alert } from '@openedx/paragon';
|
import { Hyperlink, Alert } from '@edx/paragon';
|
||||||
import { WarningFilled } from '@openedx/paragon/icons';
|
import { WarningFilled } from '@edx/paragon/icons';
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
export default function BulkEmailPendingTasksAlert(props) {
|
export default function BulkEmailPendingTasksAlert(props) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { Icon, Collapsible } from '@openedx/paragon';
|
import { Icon, Collapsible } from '@edx/paragon';
|
||||||
import { SpinnerSimple } from '@openedx/paragon/icons';
|
import { SpinnerSimple } from '@edx/paragon/icons';
|
||||||
import { getEmailTaskHistory } from './data/api';
|
import { getEmailTaskHistory } from './data/api';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
@@ -11,8 +11,7 @@ import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
|
|||||||
|
|
||||||
import './bulkEmailTaskHistory.scss';
|
import './bulkEmailTaskHistory.scss';
|
||||||
|
|
||||||
function BulkEmailTaskHistory() {
|
function BulkEmailTaskHistory({ intl }) {
|
||||||
const intl = useIntl();
|
|
||||||
const { courseId } = useParams();
|
const { courseId } = useParams();
|
||||||
|
|
||||||
const [emailTaskHistoryData, setEmailTaskHistoryData] = useState([]);
|
const [emailTaskHistoryData, setEmailTaskHistoryData] = useState([]);
|
||||||
@@ -42,14 +41,6 @@ function BulkEmailTaskHistory() {
|
|||||||
setShowHistoricalTaskContentTable(true);
|
setShowHistoricalTaskContentTable(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformDataForTable = useMemo(() => {
|
|
||||||
const tableData = emailTaskHistoryData?.map((item) => ({
|
|
||||||
...item,
|
|
||||||
created: new Date(item.created).toLocaleString(),
|
|
||||||
}));
|
|
||||||
return tableData || [];
|
|
||||||
}, [emailTaskHistoryData]);
|
|
||||||
|
|
||||||
const tableColumns = [
|
const tableColumns = [
|
||||||
{
|
{
|
||||||
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskType)}`,
|
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskType)}`,
|
||||||
@@ -104,7 +95,7 @@ function BulkEmailTaskHistory() {
|
|||||||
{showHistoricalTaskContentTable ? (
|
{showHistoricalTaskContentTable ? (
|
||||||
<BulkEmailTaskManagerTable
|
<BulkEmailTaskManagerTable
|
||||||
errorRetrievingData={errorRetrievingData}
|
errorRetrievingData={errorRetrievingData}
|
||||||
tableData={transformDataForTable}
|
tableData={emailTaskHistoryData}
|
||||||
alertWarningMessage={intl.formatMessage(messages.noTaskHistoryData)}
|
alertWarningMessage={intl.formatMessage(messages.noTaskHistoryData)}
|
||||||
alertErrorMessage={intl.formatMessage(messages.errorFetchingTaskHistoryData)}
|
alertErrorMessage={intl.formatMessage(messages.errorFetchingTaskHistoryData)}
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
@@ -118,4 +109,8 @@ function BulkEmailTaskHistory() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BulkEmailTaskHistory;
|
BulkEmailTaskHistory.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(BulkEmailTaskHistory);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import BulkEmailContentHistory from './BulkEmailContentHistory';
|
import BulkEmailContentHistory from './BulkEmailContentHistory';
|
||||||
import BulkEmailTaskHistory from './BulkEmailTaskHistory';
|
import BulkEmailTaskHistory from './BulkEmailTaskHistory';
|
||||||
@@ -9,8 +9,7 @@ import messages from './messages';
|
|||||||
import BulkEmailScheduledEmailsTable from './bulk-email-scheduled-emails-table';
|
import BulkEmailScheduledEmailsTable from './bulk-email-scheduled-emails-table';
|
||||||
import BulkEmailPendingTasksAlert from './BulkEmailPendingTasksAlert';
|
import BulkEmailPendingTasksAlert from './BulkEmailPendingTasksAlert';
|
||||||
|
|
||||||
function BulkEmailTaskManager({ courseId }) {
|
function BulkEmailTaskManager({ intl, courseId }) {
|
||||||
const intl = useIntl();
|
|
||||||
return (
|
return (
|
||||||
<div className="w-100">
|
<div className="w-100">
|
||||||
{getConfig().SCHEDULE_EMAIL_SECTION && (
|
{getConfig().SCHEDULE_EMAIL_SECTION && (
|
||||||
@@ -35,7 +34,8 @@ function BulkEmailTaskManager({ courseId }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BulkEmailTaskManager.propTypes = {
|
BulkEmailTaskManager.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
courseId: PropTypes.string.isRequired,
|
courseId: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BulkEmailTaskManager;
|
export default injectIntl(BulkEmailTaskManager);
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ActionRow, Button, ModalDialog } from '@openedx/paragon';
|
import { Button, Modal } from '@edx/paragon';
|
||||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { BulkEmailContext } from '../bulk-email-context';
|
import { BulkEmailContext } from '../bulk-email-context';
|
||||||
import { copyToEditor } from '../bulk-email-form/data/actions';
|
import { copyToEditor } from '../bulk-email-form/data/actions';
|
||||||
|
|
||||||
function ViewEmailModal({
|
function ViewEmailModal({
|
||||||
messageContent, isOpen, setModalOpen,
|
intl, messageContent, isOpen, setModalOpen,
|
||||||
}) {
|
}) {
|
||||||
const intl = useIntl();
|
|
||||||
const [, dispatch] = useContext(BulkEmailContext);
|
const [, dispatch] = useContext(BulkEmailContext);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ModalDialog
|
<Modal
|
||||||
isOpen={isOpen}
|
open={isOpen}
|
||||||
onClose={() => setModalOpen(false)}
|
title=""
|
||||||
hasCloseButton
|
body={(
|
||||||
>
|
|
||||||
<ModalDialog.Body>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="d-flex flex-row">
|
<div className="d-flex flex-row">
|
||||||
<p>{intl.formatMessage(messages.modalMessageSubject)}</p>
|
<p>{intl.formatMessage(messages.modalMessageSubject)}</p>
|
||||||
@@ -44,35 +40,30 @@ function ViewEmailModal({
|
|||||||
<div dangerouslySetInnerHTML={{ __html: messageContent.email.html_message }} />
|
<div dangerouslySetInnerHTML={{ __html: messageContent.email.html_message }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalDialog.Body>
|
)}
|
||||||
<ModalDialog.Footer>
|
onClose={() => setModalOpen(false)}
|
||||||
<ActionRow>
|
buttons={[
|
||||||
<ModalDialog.CloseButton variant="link">
|
<Button
|
||||||
<FormattedMessage id="bulk.email.tool.close.modalDialog.button" defaultMessage="Close" />
|
onClick={() => {
|
||||||
</ModalDialog.CloseButton>
|
dispatch(
|
||||||
<Button
|
copyToEditor({
|
||||||
onClick={() => {
|
emailBody: messageContent.email.html_message,
|
||||||
dispatch(
|
emailSubject: messageContent.subject,
|
||||||
copyToEditor({
|
}),
|
||||||
emailBody: messageContent.email.html_message,
|
);
|
||||||
emailSubject: messageContent.subject,
|
setModalOpen(false);
|
||||||
}),
|
}}
|
||||||
);
|
>
|
||||||
setModalOpen(false);
|
<FormattedMessage id="bulk.email.tool.copy.message.button" defaultMessage="Copy to editor" />
|
||||||
}}
|
</Button>,
|
||||||
variant="primary"
|
]}
|
||||||
>
|
/>
|
||||||
<FormattedMessage id="bulk.email.tool.copy.message.button" defaultMessage="Copy to editor" />
|
|
||||||
|
|
||||||
</Button>
|
|
||||||
</ActionRow>
|
|
||||||
</ModalDialog.Footer>
|
|
||||||
</ModalDialog>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewEmailModal.propTypes = {
|
ViewEmailModal.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
messageContent: PropTypes.shape({
|
messageContent: PropTypes.shape({
|
||||||
subject: PropTypes.string,
|
subject: PropTypes.string,
|
||||||
requester: PropTypes.string,
|
requester: PropTypes.string,
|
||||||
@@ -86,4 +77,4 @@ ViewEmailModal.propTypes = {
|
|||||||
setModalOpen: PropTypes.func.isRequired,
|
setModalOpen: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ViewEmailModal;
|
export default injectIntl(ViewEmailModal);
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
import React, {
|
import React, {
|
||||||
useCallback, useContext, useState, useEffect,
|
useCallback, useContext, useState, useEffect,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
Alert, DataTable, Icon, IconButton, useToggle,
|
Alert, DataTable, Icon, IconButton, useToggle,
|
||||||
} from '@openedx/paragon';
|
} from '@edx/paragon';
|
||||||
import {
|
import {
|
||||||
Delete, Info, Visibility, Edit,
|
Delete, Info, Visibility, Edit,
|
||||||
} from '@openedx/paragon/icons';
|
} from '@edx/paragon/icons';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { BulkEmailContext } from '../../bulk-email-context';
|
import { BulkEmailContext } from '../../bulk-email-context';
|
||||||
import { deleteScheduledEmailThunk, getScheduledBulkEmailThunk } from './data/thunks';
|
import { deleteScheduledEmailThunk, getScheduledBulkEmailThunk } from './data/thunks';
|
||||||
@@ -26,14 +26,12 @@ function flattenScheduledEmailsArray(emails) {
|
|||||||
emailId: email.courseEmail.id,
|
emailId: email.courseEmail.id,
|
||||||
task: email.task,
|
task: email.task,
|
||||||
taskDue: new Date(email.taskDue).toLocaleString(),
|
taskDue: new Date(email.taskDue).toLocaleString(),
|
||||||
taskDueUTC: email.taskDue,
|
|
||||||
...email.courseEmail,
|
...email.courseEmail,
|
||||||
targets: email.courseEmail.targets.join(', '),
|
targets: email.courseEmail.targets.join(', '),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function BulkEmailScheduledEmailsTable() {
|
function BulkEmailScheduledEmailsTable({ intl }) {
|
||||||
const intl = useIntl();
|
|
||||||
const { courseId } = useParams();
|
const { courseId } = useParams();
|
||||||
const [{ scheduledEmailsTable }, dispatch] = useContext(BulkEmailContext);
|
const [{ scheduledEmailsTable }, dispatch] = useContext(BulkEmailContext);
|
||||||
const [tableData, setTableData] = useState([]);
|
const [tableData, setTableData] = useState([]);
|
||||||
@@ -93,10 +91,10 @@ function BulkEmailScheduledEmailsTable() {
|
|||||||
const handleEditEmail = (row) => {
|
const handleEditEmail = (row) => {
|
||||||
const {
|
const {
|
||||||
original: {
|
original: {
|
||||||
htmlMessage: emailBody, subject: emailSubject, taskDueUTC, targets, schedulingId, emailId,
|
htmlMessage: emailBody, subject: emailSubject, taskDue, targets, schedulingId, emailId,
|
||||||
},
|
},
|
||||||
} = row;
|
} = row;
|
||||||
const dateTime = new Date(taskDueUTC);
|
const dateTime = new Date(taskDue);
|
||||||
const emailRecipients = targets.replaceAll('-', ':').split(', ');
|
const emailRecipients = targets.replaceAll('-', ':').split(', ');
|
||||||
const scheduleDate = formatDate(dateTime);
|
const scheduleDate = formatDate(dateTime);
|
||||||
const scheduleTime = formatTime(dateTime);
|
const scheduleTime = formatTime(dateTime);
|
||||||
@@ -197,4 +195,8 @@ function BulkEmailScheduledEmailsTable() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BulkEmailScheduledEmailsTable;
|
BulkEmailScheduledEmailsTable.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(BulkEmailScheduledEmailsTable);
|
||||||
|
|||||||
@@ -34,10 +34,6 @@ const messages = defineMessages({
|
|||||||
id: 'bulk.email.content.history.table.modal.messageBody',
|
id: 'bulk.email.content.history.table.modal.messageBody',
|
||||||
defaultMessage: 'Message:',
|
defaultMessage: 'Message:',
|
||||||
},
|
},
|
||||||
modalCloseButton: {
|
|
||||||
id: 'bulk.email.tool.close.modalDialog.button',
|
|
||||||
defaultMessage: 'Close',
|
|
||||||
},
|
|
||||||
emailHistoryTableViewMessageInstructions: {
|
emailHistoryTableViewMessageInstructions: {
|
||||||
id: 'bulk.email.content.history.table.viewMessageInstructions',
|
id: 'bulk.email.content.history.table.viewMessageInstructions',
|
||||||
defaultMessage: 'To read a sent email message, click the `View Message` button within the table.',
|
defaultMessage: 'To read a sent email message, click the `View Message` button within the table.',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
render, screen, fireEvent, cleanup, initializeMockApp,
|
render, screen, fireEvent, cleanup, act, initializeMockApp,
|
||||||
} from '../../../../setupTest';
|
} from '../../../../setupTest';
|
||||||
import { BulkEmailProvider } from '../../bulk-email-context';
|
import { BulkEmailProvider } from '../../bulk-email-context';
|
||||||
import BulkEmailContentHistory from '../BulkEmailContentHistory';
|
import BulkEmailContentHistory from '../BulkEmailContentHistory';
|
||||||
@@ -41,99 +41,105 @@ describe('BulkEmailContentHistory component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('renders a table when the button is pressed and data is returned', async () => {
|
test('renders a table when the button is pressed and data is returned', async () => {
|
||||||
const emailHistoryData = buildEmailContentHistoryData(1);
|
await act(async () => {
|
||||||
getSentEmailHistory.mockImplementation(() => emailHistoryData);
|
const emailHistoryData = buildEmailContentHistoryData(1);
|
||||||
|
getSentEmailHistory.mockImplementation(() => emailHistoryData);
|
||||||
|
|
||||||
render(renderBulkEmailContentHistory());
|
render(renderBulkEmailContentHistory());
|
||||||
|
|
||||||
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
|
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
|
||||||
fireEvent.click(showEmailContentHistoryButton);
|
fireEvent.click(showEmailContentHistoryButton);
|
||||||
|
|
||||||
// verify component structure
|
// verify component structure
|
||||||
const tableDescription = await screen.findByText(
|
const tableDescription = await screen.findByText(
|
||||||
'To read a sent email message, click the `View Message` button within the table.',
|
'To read a sent email message, click the `View Message` button within the table.',
|
||||||
);
|
);
|
||||||
expect(tableDescription).toBeTruthy();
|
expect(tableDescription).toBeTruthy();
|
||||||
|
|
||||||
// verify table structure
|
// verify table structure
|
||||||
expect(await screen.findByText('Subject')).toBeTruthy();
|
expect(await screen.findByText('Subject')).toBeTruthy();
|
||||||
expect(await screen.findByText('Sent By')).toBeTruthy();
|
expect(await screen.findByText('Sent By')).toBeTruthy();
|
||||||
expect(await screen.findByText('Sent To')).toBeTruthy();
|
expect(await screen.findByText('Sent To')).toBeTruthy();
|
||||||
expect(await screen.findByText('Time Sent')).toBeTruthy();
|
expect(await screen.findByText('Time Sent')).toBeTruthy();
|
||||||
expect(await screen.findByText('Number Sent')).toBeTruthy();
|
expect(await screen.findByText('Number Sent')).toBeTruthy();
|
||||||
|
|
||||||
// verify table contents
|
// verify table contents
|
||||||
const { emails } = emailHistoryData;
|
const { emails } = emailHistoryData;
|
||||||
const email = emails[0];
|
const email = emails[0];
|
||||||
const createdDate = new Date(email.created).toLocaleString();
|
expect(await screen.findByText(email.created)).toBeTruthy();
|
||||||
expect(await screen.findByText(createdDate)).toBeTruthy();
|
expect(await screen.findByText(email.number_sent)).toBeTruthy();
|
||||||
expect(await screen.findByText(email.number_sent)).toBeTruthy();
|
expect(await screen.findByText(email.requester)).toBeTruthy();
|
||||||
expect(await screen.findByText(email.requester)).toBeTruthy();
|
expect(await screen.findByText(email.sent_to.join(', '))).toBeTruthy();
|
||||||
expect(await screen.findByText(email.sent_to.join(', '))).toBeTruthy();
|
expect(await screen.findByText(email.email.subject)).toBeTruthy();
|
||||||
expect(await screen.findByText(email.email.subject)).toBeTruthy();
|
// verify screen reader only <span />
|
||||||
// verify screen reader only <span />
|
expect(await screen.findByText('0')).toHaveClass('sr-only');
|
||||||
expect(await screen.findByText('0')).toHaveClass('sr-only');
|
expect(await screen.findAllByText('View Message')).toBeTruthy();
|
||||||
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 () => {
|
test('renders a modal that will display the contents of the previously sent message to a user', async () => {
|
||||||
const emailHistoryData = buildEmailContentHistoryData(1);
|
await act(async () => {
|
||||||
getSentEmailHistory.mockImplementation(() => emailHistoryData);
|
const emailHistoryData = buildEmailContentHistoryData(1);
|
||||||
|
getSentEmailHistory.mockImplementation(() => emailHistoryData);
|
||||||
|
|
||||||
render(renderBulkEmailContentHistory());
|
render(renderBulkEmailContentHistory());
|
||||||
|
|
||||||
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
|
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
|
||||||
fireEvent.click(showEmailContentHistoryButton);
|
fireEvent.click(showEmailContentHistoryButton);
|
||||||
|
|
||||||
const viewMessageButton = await screen.findByText('View Message');
|
const viewMessageButton = await screen.findByText('View Message');
|
||||||
fireEvent.click(viewMessageButton);
|
fireEvent.click(viewMessageButton);
|
||||||
|
|
||||||
// verify modal components and behavior
|
// verify modal components and behavior
|
||||||
const { emails } = emailHistoryData;
|
const { emails } = emailHistoryData;
|
||||||
const email = emails[0];
|
const email = emails[0];
|
||||||
const closeButton = await screen.findAllByText('Close');
|
const closeButton = await screen.findAllByText('Close');
|
||||||
|
|
||||||
expect(closeButton).toBeTruthy();
|
expect(closeButton).toBeTruthy();
|
||||||
expect(await screen.findByText('Subject:')).toBeTruthy();
|
expect(await screen.findByText('Subject:')).toBeTruthy();
|
||||||
expect(await screen.findByText('Sent by:')).toBeTruthy();
|
expect(await screen.findByText('Sent by:')).toBeTruthy();
|
||||||
expect(await screen.findByText('Time sent:')).toBeTruthy();
|
expect(await screen.findByText('Time sent:')).toBeTruthy();
|
||||||
expect(await screen.findByText('Sent to:')).toBeTruthy();
|
expect(await screen.findByText('Sent to:')).toBeTruthy();
|
||||||
expect(await screen.findByText('Message:')).toBeTruthy();
|
expect(await screen.findByText('Message:')).toBeTruthy();
|
||||||
expect(await screen.findAllByText(email.email.subject)).toBeTruthy();
|
expect(await screen.findAllByText(email.email.subject)).toBeTruthy();
|
||||||
expect(await screen.findAllByText(email.requester)).toBeTruthy();
|
expect(await screen.findAllByText(email.requester)).toBeTruthy();
|
||||||
const createdDate = new Date(email.created).toLocaleString();
|
expect(await screen.findAllByText(email.created)).toBeTruthy();
|
||||||
expect(await screen.findAllByText(createdDate)).toBeTruthy();
|
expect(await screen.findAllByText(email.sent_to.join(', '))).toBeTruthy();
|
||||||
expect(await screen.findAllByText(email.sent_to.join(', '))).toBeTruthy();
|
// .replace() call strips the HTML tags from the string
|
||||||
// .replace() call strips the HTML tags from the string
|
expect(await screen.findByText(email.email.html_message.replace(/<[^>]*>?/gm, ''))).toBeTruthy();
|
||||||
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 () => {
|
test('renders a warning Alert when the button is pressed but there is no data to display', async () => {
|
||||||
const emailHistoryData = buildEmailContentHistoryData(0);
|
await act(async () => {
|
||||||
getSentEmailHistory.mockImplementation(() => emailHistoryData);
|
const emailHistoryData = buildEmailContentHistoryData(0);
|
||||||
// render the component
|
getSentEmailHistory.mockImplementation(() => emailHistoryData);
|
||||||
render(renderBulkEmailContentHistory());
|
// render the component
|
||||||
// press the `show sent email history` button to initiate data retrieval
|
render(renderBulkEmailContentHistory());
|
||||||
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
|
// press the `show sent email history` button to initiate data retrieval
|
||||||
fireEvent.click(showEmailContentHistoryButton);
|
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
|
||||||
// verify that an alert is displayed since the array of tasks is empty
|
fireEvent.click(showEmailContentHistoryButton);
|
||||||
const alertMessage = await screen.findByText('There is no email history for this course.');
|
// verify that an alert is displayed since the array of tasks is empty
|
||||||
expect(alertMessage).toBeTruthy();
|
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 () => {
|
test('renders an error Alert when the button is pressed and an error occurs retrieving data', async () => {
|
||||||
getSentEmailHistory.mockImplementation(() => {
|
await act(async () => {
|
||||||
throw new Error();
|
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();
|
||||||
});
|
});
|
||||||
// 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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ describe('BulkEmailPendingTasks component', () => {
|
|||||||
expect(await screen.findByText('State')).toBeTruthy();
|
expect(await screen.findByText('State')).toBeTruthy();
|
||||||
expect(await screen.findByText('Status')).toBeTruthy();
|
expect(await screen.findByText('Status')).toBeTruthy();
|
||||||
expect(await screen.findByText('Task Progress')).toBeTruthy();
|
expect(await screen.findByText('Task Progress')).toBeTruthy();
|
||||||
expect(await screen.findAllByText('Showing 1 - 1 of 1.')).toBeTruthy();
|
expect(await screen.findAllByText('Showing 1 of 1.')).toBeTruthy();
|
||||||
|
|
||||||
// verification of table contents
|
// verification of table contents
|
||||||
const { tasks } = pendingInstructorTaskData;
|
const { tasks } = pendingInstructorTaskData;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
render, screen, fireEvent, cleanup, initializeMockApp,
|
render, screen, fireEvent, cleanup, act, initializeMockApp,
|
||||||
} from '../../../../setupTest';
|
} from '../../../../setupTest';
|
||||||
import BulkEmailTaskHistory from '../BulkEmailTaskHistory';
|
import BulkEmailTaskHistory from '../BulkEmailTaskHistory';
|
||||||
import { getEmailTaskHistory } from '../data/api';
|
import { getEmailTaskHistory } from '../data/api';
|
||||||
@@ -32,66 +32,71 @@ describe('BulkEmailTaskHistory component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('renders a table properly when the button is pressed and data is returned', async () => {
|
test('renders a table properly when the button is pressed and data is returned', async () => {
|
||||||
// build our mocked response
|
await act(async () => {
|
||||||
const taskHistoryData = buildEmailTaskHistoryData(1);
|
// build our mocked response
|
||||||
getEmailTaskHistory.mockImplementation(() => taskHistoryData);
|
const taskHistoryData = buildEmailTaskHistoryData(1);
|
||||||
// render the component
|
getEmailTaskHistory.mockImplementation(() => taskHistoryData);
|
||||||
render(<BulkEmailTaskHistory />);
|
// render the component
|
||||||
// press the `show task history button` to initiate data retrieval and rendering of the table in our component
|
render(<BulkEmailTaskHistory />);
|
||||||
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
|
// press the `show task history button` to initiate data retrieval and rendering of the table in our component
|
||||||
fireEvent.click(showTaskHistoryButton);
|
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
|
||||||
// verification of table structure
|
fireEvent.click(showTaskHistoryButton);
|
||||||
expect(await screen.findByText('Task Type')).toBeTruthy();
|
// verification of table structure
|
||||||
expect(await screen.findByText('Task Inputs')).toBeTruthy();
|
expect(await screen.findByText('Task Type')).toBeTruthy();
|
||||||
expect(await screen.findByText('Task Id')).toBeTruthy();
|
expect(await screen.findByText('Task Inputs')).toBeTruthy();
|
||||||
expect(await screen.findByText('Requester')).toBeTruthy();
|
expect(await screen.findByText('Task Id')).toBeTruthy();
|
||||||
expect(await screen.findByText('Submitted')).toBeTruthy();
|
expect(await screen.findByText('Requester')).toBeTruthy();
|
||||||
expect(await screen.findByText('Duration (seconds)')).toBeTruthy();
|
expect(await screen.findByText('Submitted')).toBeTruthy();
|
||||||
expect(await screen.findByText('State')).toBeTruthy();
|
expect(await screen.findByText('Duration (seconds)')).toBeTruthy();
|
||||||
expect(await screen.findByText('Status')).toBeTruthy();
|
expect(await screen.findByText('State')).toBeTruthy();
|
||||||
expect(await screen.findByText('Task Progress')).toBeTruthy();
|
expect(await screen.findByText('Status')).toBeTruthy();
|
||||||
expect(await screen.findAllByText('Showing 1 - 1 of 1.')).toBeTruthy();
|
expect(await screen.findByText('Task Progress')).toBeTruthy();
|
||||||
// verification of row contents
|
expect(await screen.findAllByText('Showing 1 of 1.')).toBeTruthy();
|
||||||
const { tasks } = taskHistoryData;
|
// verification of row contents
|
||||||
const task = tasks[0];
|
const { tasks } = taskHistoryData;
|
||||||
const createdDate = new Date(task.created).toLocaleString();
|
const task = tasks[0];
|
||||||
expect(await screen.findByText(createdDate)).toBeTruthy();
|
expect(await screen.findByText(task.created)).toBeTruthy();
|
||||||
expect(await screen.findByText(task.duration_sec)).toBeTruthy();
|
expect(await screen.findByText(task.duration_sec)).toBeTruthy();
|
||||||
expect(await screen.findByText(task.requester)).toBeTruthy();
|
expect(await screen.findByText(task.requester)).toBeTruthy();
|
||||||
expect(await screen.findByText(task.status)).toBeTruthy();
|
expect(await screen.findByText(task.status)).toBeTruthy();
|
||||||
expect(await screen.findByText(task.task_id)).toBeTruthy();
|
expect(await screen.findByText(task.task_id)).toBeTruthy();
|
||||||
expect(await screen.findByText(task.task_input)).toBeTruthy();
|
expect(await screen.findByText(task.task_input)).toBeTruthy();
|
||||||
expect(await screen.findByText(task.task_message)).toBeTruthy();
|
expect(await screen.findByText(task.task_message)).toBeTruthy();
|
||||||
expect(await screen.findByText(task.task_state)).toBeTruthy();
|
expect(await screen.findByText(task.task_state)).toBeTruthy();
|
||||||
expect(await screen.findByText(task.task_type)).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 () => {
|
test('renders a warning Alert when the button is pressed but there is no data to display', async () => {
|
||||||
const taskHistoryData = buildEmailTaskHistoryData(0);
|
await act(async () => {
|
||||||
getEmailTaskHistory.mockImplementation(() => taskHistoryData);
|
const taskHistoryData = buildEmailTaskHistoryData(0);
|
||||||
// render the component
|
getEmailTaskHistory.mockImplementation(() => taskHistoryData);
|
||||||
render(<BulkEmailTaskHistory />);
|
// render the component
|
||||||
// press the `show task history` button to initiate data retrieval
|
render(<BulkEmailTaskHistory />);
|
||||||
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
|
// press the `show task history` button to initiate data retrieval
|
||||||
fireEvent.click(showTaskHistoryButton);
|
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
|
||||||
// verify that an alert is displayed since the array of tasks is empty
|
fireEvent.click(showTaskHistoryButton);
|
||||||
const alertMessage = await screen.findByText('There is no email task history for this course.');
|
// verify that an alert is displayed since the array of tasks is empty
|
||||||
expect(alertMessage).toBeTruthy();
|
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 () => {
|
test('renders an error Alert when the button is pressed and an error occurs retrieving data', async () => {
|
||||||
getEmailTaskHistory.mockImplementation(() => {
|
await act(async () => {
|
||||||
throw new Error();
|
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();
|
||||||
});
|
});
|
||||||
// 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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ActionRow, AlertModal, Button } from '@openedx/paragon';
|
import { ActionRow, AlertModal, Button } from '@edx/paragon';
|
||||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
function TaskAlertModal(props) {
|
function TaskAlertModal(props) {
|
||||||
const {
|
const {
|
||||||
isOpen, close, alertMessage,
|
isOpen, close, alertMessage, intl,
|
||||||
} = props;
|
} = props;
|
||||||
const intl = useIntl();
|
|
||||||
const messages = {
|
const messages = {
|
||||||
taskAlertTitle: {
|
taskAlertTitle: {
|
||||||
id: 'bulk.email.task.alert.title',
|
id: 'bulk.email.task.alert.title',
|
||||||
@@ -57,6 +57,7 @@ TaskAlertModal.propTypes = {
|
|||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
close: PropTypes.func.isRequired,
|
close: PropTypes.func.isRequired,
|
||||||
alertMessage: PropTypes.node.isRequired,
|
alertMessage: PropTypes.node.isRequired,
|
||||||
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TaskAlertModal;
|
export default injectIntl(TaskAlertModal);
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import 'tinymce/plugins/image';
|
|||||||
import 'tinymce/plugins/codesample';
|
import 'tinymce/plugins/codesample';
|
||||||
import '@edx/tinymce-language-selector';
|
import '@edx/tinymce-language-selector';
|
||||||
|
|
||||||
import contentUiCss from 'tinymce/skins/ui/oxide/content.css?raw';
|
import contentUiCss from 'tinymce/skins/ui/oxide/content.css';
|
||||||
import contentCss from 'tinymce/skins/content/default/content.css?raw';
|
import contentCss from 'tinymce/skins/content/default/content.css';
|
||||||
|
|
||||||
export default function TextEditor(props) {
|
export default function TextEditor(props) {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
import { Button, Icon } from '@openedx/paragon';
|
import { Button, Icon } from '@edx/paragon';
|
||||||
import { ArrowBack } from '@openedx/paragon/icons';
|
import { ArrowBack } from '@edx/paragon/icons';
|
||||||
|
|
||||||
export default function BackToInstructor(props) {
|
export default function BackToInstructor(props) {
|
||||||
const { courseId } = props;
|
const { courseId } = props;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { Nav } from '@openedx/paragon';
|
import { Nav } from '@edx/paragon';
|
||||||
|
|
||||||
export default function NavigationTabs(props) {
|
export default function NavigationTabs(props) {
|
||||||
const { tabData } = props;
|
const { tabData } = props;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||||
import { FooterSlot } from '@edx/frontend-component-footer';
|
import Footer from '@edx/frontend-component-footer';
|
||||||
import { Spinner } from '@openedx/paragon';
|
import { Spinner } from '@edx/paragon';
|
||||||
|
|
||||||
import { getCohorts, getCourseHomeCourseMetadata } from './data/api';
|
import { getCohorts, getCourseHomeCourseMetadata } from './data/api';
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ export default function PageContainer(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
org, number, title, tabs, originalUserIsStaff, courseModes,
|
org, number, title, tabs, originalUserIsStaff,
|
||||||
} = metadataResponse;
|
} = metadataResponse;
|
||||||
const { cohorts } = cohortsResponse;
|
const { cohorts } = cohortsResponse;
|
||||||
|
|
||||||
@@ -48,7 +48,6 @@ export default function PageContainer(props) {
|
|||||||
number,
|
number,
|
||||||
title,
|
title,
|
||||||
originalUserIsStaff,
|
originalUserIsStaff,
|
||||||
courseModes,
|
|
||||||
tabs: [...tabs],
|
tabs: [...tabs],
|
||||||
cohorts: cohorts.map(({ name }) => name),
|
cohorts: cohorts.map(({ name }) => name),
|
||||||
});
|
});
|
||||||
@@ -67,12 +66,10 @@ export default function PageContainer(props) {
|
|||||||
courseNumber={courseMetadata.number}
|
courseNumber={courseMetadata.number}
|
||||||
courseTitle={courseMetadata.title}
|
courseTitle={courseMetadata.title}
|
||||||
/>
|
/>
|
||||||
<div className="pb-3 container">
|
<main>
|
||||||
<main id="main-content">
|
{children}
|
||||||
{children}
|
</main>
|
||||||
</main>
|
<Footer />
|
||||||
</div>
|
|
||||||
<FooterSlot />
|
|
||||||
</>
|
</>
|
||||||
</CourseMetadataContext.Provider>
|
</CourseMetadataContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Factory } from 'rosie';
|
import { Factory } from 'rosie';
|
||||||
import {
|
import {
|
||||||
cleanup, initializeMockApp, render, screen,
|
act, cleanup, initializeMockApp, render, screen,
|
||||||
} from '../../../setupTest';
|
} from '../../../setupTest';
|
||||||
|
|
||||||
import PageContainer from '../PageContainer';
|
import PageContainer from '../PageContainer';
|
||||||
@@ -32,32 +32,36 @@ describe('PageContainer', () => {
|
|||||||
afterEach(cleanup);
|
afterEach(cleanup);
|
||||||
|
|
||||||
test('PageContainer renders properly when given course metadata', async () => {
|
test('PageContainer renders properly when given course metadata', async () => {
|
||||||
const cohorts = { cohorts: [Factory.build('cohort'), Factory.build('cohort')] };
|
await act(async () => {
|
||||||
const courseMetadata = Factory.build('courseMetadata');
|
const cohorts = { cohorts: [Factory.build('cohort'), Factory.build('cohort')] };
|
||||||
|
const courseMetadata = Factory.build('courseMetadata');
|
||||||
|
|
||||||
getCohorts.mockImplementation(() => cohorts);
|
getCohorts.mockImplementation(() => cohorts);
|
||||||
getCourseHomeCourseMetadata.mockImplementation(() => courseMetadata);
|
getCourseHomeCourseMetadata.mockImplementation(() => courseMetadata);
|
||||||
|
|
||||||
render(<PageContainer />);
|
render(<PageContainer />);
|
||||||
|
|
||||||
// Look for the org, title, and number of the course, which should be displayed in the Header.
|
// 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.org} ${courseMetadata.number}`)).toBeTruthy();
|
||||||
expect(await screen.findByText(courseMetadata.title)).toBeTruthy();
|
expect(await screen.findByText(courseMetadata.title)).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('PageContainer renders children nested within it.', async () => {
|
test('PageContainer renders children nested within it.', async () => {
|
||||||
const cohorts = { cohorts: [Factory.build('cohort'), Factory.build('cohort')] };
|
await act(async () => {
|
||||||
const courseMetadata = Factory.build('courseMetadata');
|
const cohorts = { cohorts: [Factory.build('cohort'), Factory.build('cohort')] };
|
||||||
|
const courseMetadata = Factory.build('courseMetadata');
|
||||||
|
|
||||||
getCohorts.mockImplementation(() => cohorts);
|
getCohorts.mockImplementation(() => cohorts);
|
||||||
getCourseHomeCourseMetadata.mockImplementation(() => courseMetadata);
|
getCourseHomeCourseMetadata.mockImplementation(() => courseMetadata);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<span>Test Text</span>
|
<span>Test Text</span>
|
||||||
</PageContainer>,
|
</PageContainer>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await screen.findByText('Test Text')).toBeTruthy();
|
expect(await screen.findByText('Test Text')).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export default [];
|
|
||||||
32
src/i18n/index.jsx
Normal file
32
src/i18n/index.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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 messages = {
|
||||||
|
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 messages;
|
||||||
1
src/i18n/messages/ar.json
Normal file
1
src/i18n/messages/ar.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
src/i18n/messages/ca.json
Normal file
1
src/i18n/messages/ca.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
src/i18n/messages/es_419.json
Normal file
1
src/i18n/messages/es_419.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
src/i18n/messages/fr.json
Normal file
1
src/i18n/messages/fr.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
src/i18n/messages/he.json
Normal file
1
src/i18n/messages/he.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
src/i18n/messages/id.json
Normal file
1
src/i18n/messages/id.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
src/i18n/messages/ko_kr.json
Normal file
1
src/i18n/messages/ko_kr.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
src/i18n/messages/pl.json
Normal file
1
src/i18n/messages/pl.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
src/i18n/messages/pt_br.json
Normal file
1
src/i18n/messages/pt_br.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
src/i18n/messages/ru.json
Normal file
1
src/i18n/messages/ru.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
src/i18n/messages/th.json
Normal file
1
src/i18n/messages/th.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
src/i18n/messages/uk.json
Normal file
1
src/i18n/messages/uk.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
src/i18n/messages/zh_CN.json
Normal file
1
src/i18n/messages/zh_CN.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -5,51 +5,41 @@ import {
|
|||||||
APP_INIT_ERROR, APP_READY, subscribe, initialize, mergeConfig, getConfig,
|
APP_INIT_ERROR, APP_READY, subscribe, initialize, mergeConfig, getConfig,
|
||||||
} from '@edx/frontend-platform';
|
} from '@edx/frontend-platform';
|
||||||
import { AppProvider, AuthenticatedPageRoute, ErrorPage } from '@edx/frontend-platform/react';
|
import { AppProvider, AuthenticatedPageRoute, ErrorPage } from '@edx/frontend-platform/react';
|
||||||
import { StrictMode } from 'react';
|
import ReactDOM from 'react-dom';
|
||||||
import { createRoot } from 'react-dom/client';
|
|
||||||
|
|
||||||
|
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||||
|
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||||
|
import { messages as paragonMessages } from '@edx/paragon';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Switch } from 'react-router-dom';
|
||||||
import messages from './i18n';
|
import appMessages from './i18n';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
import BulkEmailTool from './components/bulk-email-tool';
|
import BulkEmailTool from './components/bulk-email-tool';
|
||||||
import PageContainer from './components/page-container/PageContainer';
|
import PageContainer from './components/page-container/PageContainer';
|
||||||
|
|
||||||
subscribe(APP_READY, () => {
|
subscribe(APP_READY, () => {
|
||||||
const root = createRoot(document.getElementById('root'));
|
ReactDOM.render(
|
||||||
|
<AppProvider>
|
||||||
root.render(
|
<Helmet>
|
||||||
<StrictMode>
|
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||||
<AppProvider>
|
</Helmet>
|
||||||
<Helmet>
|
<div className="pb-3 container">
|
||||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
<Switch>
|
||||||
</Helmet>
|
<AuthenticatedPageRoute path="/courses/:courseId/bulk_email">
|
||||||
<Routes>
|
<PageContainer>
|
||||||
<Route
|
<BulkEmailTool />
|
||||||
path="/courses/:courseId/bulk_email"
|
</PageContainer>
|
||||||
element={(
|
</AuthenticatedPageRoute>
|
||||||
<AuthenticatedPageRoute>
|
</Switch>
|
||||||
<PageContainer>
|
</div>
|
||||||
<BulkEmailTool />
|
</AppProvider>,
|
||||||
</PageContainer>
|
document.getElementById('root'),
|
||||||
</AuthenticatedPageRoute>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Routes>
|
|
||||||
</AppProvider>
|
|
||||||
</StrictMode>,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
subscribe(APP_INIT_ERROR, (error) => {
|
subscribe(APP_INIT_ERROR, (error) => {
|
||||||
const root = createRoot(document.getElementById('root'));
|
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
|
||||||
|
|
||||||
root.render(
|
|
||||||
<StrictMode>
|
|
||||||
<ErrorPage message={error.message} />
|
|
||||||
</StrictMode>,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
initialize({
|
initialize({
|
||||||
@@ -64,5 +54,5 @@ initialize({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
messages,
|
messages: [appMessages, headerMessages, footerMessages, paragonMessages],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
|
@import "~@edx/brand/paragon/fonts";
|
||||||
|
@import "~@edx/brand/paragon/variables";
|
||||||
|
@import "~@edx/paragon/scss/core/core";
|
||||||
|
@import "~@edx/brand/paragon/overrides";
|
||||||
|
|
||||||
@import "~@edx/frontend-component-header/dist/index";
|
@import "~@edx/frontend-component-header/dist/index";
|
||||||
@import "~@edx/frontend-component-footer/dist/footer";
|
@import "~@edx/frontend-component-footer/dist/footer";
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
with a simple custom footer
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
```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.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,3 +0,0 @@
|
|||||||
# `frontend-app-communications` Plugin Slots
|
|
||||||
|
|
||||||
* [`org.openedx.frontend.layout.footer.v1`](./FooterSlot/)
|
|
||||||
@@ -8,14 +8,12 @@ import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform
|
|||||||
import { configure as configureLogging, MockLoggingService } from '@edx/frontend-platform/logging';
|
import { configure as configureLogging, MockLoggingService } from '@edx/frontend-platform/logging';
|
||||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||||
import { configure as configureAuth, MockAuthService } from '@edx/frontend-platform/auth';
|
import { configure as configureAuth, MockAuthService } from '@edx/frontend-platform/auth';
|
||||||
import messages from './i18n';
|
import appMessages from './i18n';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/react/hooks', () => ({
|
jest.mock('@edx/frontend-platform/react/hooks', () => ({
|
||||||
...jest.requireActual('@edx/frontend-platform/react/hooks'),
|
...jest.requireActual('@edx/frontend-platform/react/hooks'),
|
||||||
useTrackColorSchemeChoice: jest.fn(),
|
useTrackColorSchemeChoice: jest.fn(),
|
||||||
useParagonTheme: () => [{ isThemeLoaded: true }, jest.fn()],
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
writable: true,
|
writable: true,
|
||||||
value: jest.fn().mockImplementation((query) => ({
|
value: jest.fn().mockImplementation((query) => ({
|
||||||
@@ -30,8 +28,6 @@ Object.defineProperty(window, 'matchMedia', {
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
global.Date.prototype.toLocaleDateString = jest.fn();
|
|
||||||
|
|
||||||
export function initializeMockApp() {
|
export function initializeMockApp() {
|
||||||
mergeConfig({
|
mergeConfig({
|
||||||
// MICROBA-1505: Remove this when we remove the flag from config
|
// MICROBA-1505: Remove this when we remove the flag from config
|
||||||
@@ -51,7 +47,7 @@ export function initializeMockApp() {
|
|||||||
const i18nService = configureI18n({
|
const i18nService = configureI18n({
|
||||||
config: getConfig(),
|
config: getConfig(),
|
||||||
loggingService,
|
loggingService,
|
||||||
messages,
|
messages: [appMessages],
|
||||||
});
|
});
|
||||||
|
|
||||||
const authService = configureAuth(MockAuthService, { config: getConfig(), loggingService });
|
const authService = configureAuth(MockAuthService, { config: getConfig(), loggingService });
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const { createConfig } = require('@openedx/frontend-build');
|
const { createConfig } = require('@edx/frontend-build');
|
||||||
|
|
||||||
const config = createConfig('webpack-dev');
|
const config = createConfig('webpack-dev');
|
||||||
|
|
||||||
@@ -22,13 +22,8 @@ const webpack5esmInteropRule = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const rawAssetRule = {
|
|
||||||
resourceQuery: /raw/,
|
|
||||||
type: 'asset/source',
|
|
||||||
};
|
|
||||||
|
|
||||||
const otherRules = config.module.rules;
|
const otherRules = config.module.rules;
|
||||||
|
|
||||||
config.module.rules = [rawAssetRule, webpack5esmInteropRule, ...otherRules];
|
config.module.rules = [webpack5esmInteropRule, ...otherRules];
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { createConfig } = require('@openedx/frontend-build');
|
const { createConfig } = require('@edx/frontend-build');
|
||||||
|
|
||||||
const config = createConfig('webpack-prod');
|
const config = createConfig('webpack-prod');
|
||||||
|
|
||||||
@@ -14,13 +14,8 @@ const webpack5esmInteropRule = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const rawAssetRule = {
|
|
||||||
resourceQuery: /raw/,
|
|
||||||
type: 'asset/source',
|
|
||||||
};
|
|
||||||
|
|
||||||
const otherRules = config.module.rules;
|
const otherRules = config.module.rules;
|
||||||
|
|
||||||
config.module.rules = [rawAssetRule, webpack5esmInteropRule, ...otherRules];
|
config.module.rules = [webpack5esmInteropRule, ...otherRules];
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
Reference in New Issue
Block a user