Compare commits
49 Commits
mashal-m/p
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cb95abe14 | ||
|
|
4b4ba11932 | ||
|
|
9279a3e4ce | ||
|
|
2a5cf010f8 | ||
|
|
6dd835d63f | ||
|
|
87c1cb5bd2 | ||
|
|
5875631a6c | ||
|
|
176a95c352 | ||
|
|
b24971572d | ||
|
|
20f212501f | ||
|
|
852d0ef2d9 | ||
|
|
809112d4b2 | ||
|
|
d249b5e4aa | ||
|
|
dad6c84d46 | ||
|
|
5f61578e28 | ||
|
|
da30730662 | ||
|
|
6e4ae9d976 | ||
|
|
e863eef77d | ||
|
|
ff900335a1 | ||
|
|
c0dc0cbad6 | ||
|
|
83aec7318e | ||
|
|
a037f5b340 | ||
|
|
91402fbf12 | ||
|
|
7a299eb064 | ||
|
|
90e8eeeb50 | ||
|
|
f25ce0b95d | ||
|
|
98db1a4a35 | ||
|
|
cf4b632c55 | ||
|
|
0720b2feae | ||
|
|
56e1781004 | ||
|
|
83eb21bb9a | ||
|
|
18ddb35e1e | ||
|
|
ae93b68d57 | ||
|
|
7ce2a61233 | ||
|
|
2de12997ce | ||
|
|
f6c8cfa906 | ||
|
|
db181a7945 | ||
|
|
840449f71b | ||
|
|
01f1793414 | ||
|
|
ce45c8012d | ||
|
|
7224785682 | ||
|
|
6d27b7082d | ||
|
|
c90bdb1ac7 | ||
|
|
090968e2f2 | ||
|
|
765857f380 | ||
|
|
0eec29a458 | ||
|
|
c2b4d04b5d | ||
|
|
05d16cf231 | ||
|
|
81a5f89f36 |
2
.env
2
.env
@@ -19,3 +19,5 @@ SEGMENT_KEY=''
|
||||
SITE_NAME=''
|
||||
USER_INFO_COOKIE_NAME=''
|
||||
SCHEDULE_EMAIL_SECTION=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -20,3 +20,5 @@ SEGMENT_KEY=''
|
||||
SITE_NAME=localhost
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
SCHEDULE_EMAIL_SECTION='true'
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -18,3 +18,5 @@ SEGMENT_KEY=''
|
||||
SITE_NAME=localhost
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
SCHEDULE_EMAIL_SECTION='true'
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint', {
|
||||
rules: {
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ temp/babel-plugin-react-intl
|
||||
*~
|
||||
/temp
|
||||
/.vscode
|
||||
src/i18n/messages/
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:open-edx:p:edx-platform:r:frontend-app-communications]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
39
Makefile
39
Makefile
@@ -1,20 +1,17 @@
|
||||
export TRANSIFEX_RESOURCE = frontend-app-communications
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
|
||||
precommit:
|
||||
npm run lint
|
||||
npm audit
|
||||
|
||||
requirements:
|
||||
npm install
|
||||
npm ci
|
||||
|
||||
i18n.extract:
|
||||
# Pulling display strings from .jsx files into .json files...
|
||||
@@ -32,35 +29,19 @@ detect_changed_source_translations:
|
||||
# Checking for changed translations...
|
||||
git diff --exit-code $(i18n)
|
||||
|
||||
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||
push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
else
|
||||
# Experimental: OEP-58 Pulls translations using atlas
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-app-communications/src/i18n/messages:frontend-app-communications
|
||||
&& atlas pull $(ATLAS_OPTIONS) \
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-lib-content-components/src/i18n/messages:frontend-lib-content-components \
|
||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||
translations/frontend-app-communications/src/i18n/messages:frontend-app-communications
|
||||
|
||||
$(intl_imports) frontend-component-header frontend-component-footer paragon frontend-app-communications
|
||||
endif
|
||||
$(intl_imports) frontend-component-header frontend-component-footer paragon frontend-lib-content-components frontend-platform frontend-app-communications
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
147
README.rst
147
README.rst
@@ -1,45 +1,48 @@
|
||||
|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
|
||||
==============================
|
||||
#############################
|
||||
|
||||
Please tag **edx-aperture** on any PRs or issues. Thanks!
|
||||
|license-badge| |status-badge| |ci-badge| |codecov-badge|
|
||||
|
||||
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
|
||||
*******
|
||||
|
||||
A tool used by course teams to communicate with their learners. The interface for anything related to instructor to learner communications. Instructor bulk email, for example.
|
||||
|
||||
Getting started
|
||||
------------
|
||||
|
||||
For now, this repo is not intergrated with devstack. You'll be running the app locally and not through docker. This does make setup a little easier.
|
||||
|
||||
1. Clone the repo into your usual workspace
|
||||
Cloning and Startup
|
||||
===================
|
||||
|
||||
.. code-block::
|
||||
1. Clone your new repo:
|
||||
|
||||
mkdir -p ~/workspace/
|
||||
cd ~/workspace/
|
||||
git clone https://github.com/edx/frontend-app-communications.git
|
||||
``git clone https://github.com/edx/frontend-app-communications.git``
|
||||
|
||||
2. Install frontend dependencies
|
||||
2. Use node v18.x.
|
||||
|
||||
.. code-block::
|
||||
The current version of the micro-frontend build scripts support node 18.
|
||||
Using other major versions of node *may* work, but this is unsupported. For
|
||||
convenience, this repository includes an .nvmrc file to help in setting the
|
||||
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
||||
|
||||
npm i
|
||||
3. Install npm dependencies:
|
||||
|
||||
3. Start the devserver. The app will be running at ``localhost:1984``, or whatever port you change it too.
|
||||
``cd frontend-app-communications && npm install``
|
||||
|
||||
.. code-block::
|
||||
4. Update the application port to use for local development:
|
||||
|
||||
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::
|
||||
|
||||
npm start
|
||||
|
||||
npm start
|
||||
|
||||
Environment Variables/Setup Notes
|
||||
---------------------------------
|
||||
@@ -65,3 +68,101 @@ Tests use `jest` and `react-test-library`. To run all the tests for this repo:
|
||||
.. code-block::
|
||||
|
||||
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 see 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
|
||||
18
catalog-info.yaml
Normal file
18
catalog-info.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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: ""
|
||||
spec:
|
||||
owner: group:committers-frontend
|
||||
type: "service"
|
||||
lifecycle: "production"
|
||||
@@ -1,4 +1,4 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
// setupFilesAfterEnv is used after the jest environment has been loaded. In general this is what you want.
|
||||
|
||||
@@ -23,9 +23,9 @@ module.exports = {
|
||||
**********************************************************************************************/
|
||||
|
||||
// { moduleName: '@edx/brand', dir: '../brand-openedx' }, // replace with your brand checkout
|
||||
// { moduleName: '@edx/paragon/scss/core', dir: '../paragon', dist: 'scss/core' },
|
||||
// { moduleName: '@edx/paragon/icons', dir: '../paragon', dist: 'icons' },
|
||||
// { moduleName: '@edx/paragon', dir: '../paragon', dist: 'dist' },
|
||||
// { moduleName: '@openedx/paragon/scss/core', dir: '../paragon', dist: 'scss/core' },
|
||||
// { moduleName: '@openedx/paragon/icons', dir: '../paragon', dist: 'icons' },
|
||||
// { moduleName: '@openedx/paragon', dir: '../paragon', dist: 'dist' },
|
||||
// { moduleName: '@edx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
|
||||
],
|
||||
};
|
||||
|
||||
17472
package-lock.json
generated
17472
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -11,7 +11,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
@@ -33,17 +33,19 @@
|
||||
"url": "https://github.com/edx/frontend-app-communications/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "^12.0.0",
|
||||
"@edx/frontend-component-header": "^4.0.0",
|
||||
"@edx/frontend-platform": "^4.0.1",
|
||||
"@edx/paragon": "^20.20.0",
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-header": "5.0.2",
|
||||
"@edx/frontend-platform": "7.0.1",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@edx/tinymce-language-selector": "1.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.1.2",
|
||||
"@openedx/frontend-slot-footer": "^1.0.2",
|
||||
"@openedx/paragon": "^22.0.0",
|
||||
"@tinymce/tinymce-react": "3.14.0",
|
||||
"axios": "0.27.2",
|
||||
"classnames": "2.3.2",
|
||||
@@ -51,19 +53,20 @@
|
||||
"jquery": "3.6.1",
|
||||
"popper.js": "1.16.1",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "5.3.4",
|
||||
"react-router-dom": "5.3.4",
|
||||
"react-router": "6.15.0",
|
||||
"react-router-dom": "6.15.0",
|
||||
"redux": "4.2.0",
|
||||
"regenerator-runtime": "0.13.11",
|
||||
"tinymce": "5.10.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.2.0",
|
||||
"@edx/frontend-build": "^12.7.0",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@openedx/frontend-build": "13.0.27",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"axios-mock-adapter": "1.21.2",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["@edx"],
|
||||
"matchPackagePatterns": ["@edx", "@openedx"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
import { Container } from '@edx/paragon';
|
||||
import { Container } from '@openedx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import BulkEmailTaskManager from './bulk-email-task-manager/BulkEmailTaskManager';
|
||||
import NavigationTabs from '../navigation-tabs/NavigationTabs';
|
||||
@@ -24,7 +24,7 @@ export default function BulkEmailTool() {
|
||||
<Container size="md">
|
||||
<BackToInstructor courseId={courseId} />
|
||||
<div className="row pb-4.5">
|
||||
<h1 className="text-primary-500" id="main-content">
|
||||
<h1 className="text-primary-500">
|
||||
<FormattedMessage
|
||||
id="bulk.email.send.email.header"
|
||||
defaultMessage="Send an email"
|
||||
@@ -33,7 +33,11 @@ export default function BulkEmailTool() {
|
||||
</h1>
|
||||
</div>
|
||||
<div className="row">
|
||||
<BulkEmailForm courseId={courseId} cohorts={courseMetadata.cohorts} />
|
||||
<BulkEmailForm
|
||||
courseId={courseId}
|
||||
cohorts={courseMetadata.cohorts}
|
||||
courseModes={courseMetadata.courseModes}
|
||||
/>
|
||||
</div>
|
||||
<div className="row py-5">
|
||||
<BulkEmailTaskManager courseId={courseId} />
|
||||
|
||||
@@ -4,10 +4,10 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
Button,
|
||||
Form, Icon, StatefulButton, Toast, useToggle,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import {
|
||||
SpinnerSimple, Cancel, Send, Event, Check,
|
||||
} from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
@@ -47,7 +47,12 @@ const FORM_ACTIONS = {
|
||||
};
|
||||
|
||||
function BulkEmailForm(props) {
|
||||
const { courseId, cohorts, intl } = props;
|
||||
const {
|
||||
courseId,
|
||||
cohorts,
|
||||
courseModes,
|
||||
intl,
|
||||
} = props;
|
||||
const [{ editor }, dispatch] = useContext(BulkEmailContext);
|
||||
const [emailFormStatus, setEmailFormStatus] = useState(FORM_SUBMIT_STATES.DEFAULT);
|
||||
const [emailFormValidation, setEmailFormValidation] = useState({
|
||||
@@ -272,10 +277,14 @@ function BulkEmailForm(props) {
|
||||
handleCheckboxes={onRecipientChange}
|
||||
additionalCohorts={cohorts}
|
||||
isValid={emailFormValidation.recipients}
|
||||
courseModes={courseModes}
|
||||
/>
|
||||
<Form.Group controlId="emailSubject">
|
||||
<Form.Label className="h3 text-primary-500">{intl.formatMessage(messages.bulkEmailSubjectLabel)}</Form.Label>
|
||||
<Form.Control name="emailSubject" className="w-lg-50" onChange={onFormChange} value={editor.emailSubject} />
|
||||
<Form.Control name="emailSubject" className="w-lg-50" onChange={onFormChange} value={editor.emailSubject} maxLength={128} />
|
||||
<Form.Control.Feedback className="px-3" type="default">
|
||||
{intl.formatMessage(messages.bulkEmailFormSubjectTip)}
|
||||
</Form.Control.Feedback>
|
||||
{!emailFormValidation.subject && (
|
||||
<Form.Control.Feedback className="px-3" hasIcon type="invalid">
|
||||
{intl.formatMessage(messages.bulkEmailFormSubjectError)}
|
||||
@@ -384,6 +393,12 @@ BulkEmailForm.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
cohorts: PropTypes.arrayOf(PropTypes.string),
|
||||
intl: intlShape.isRequired,
|
||||
courseModes: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
slug: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(BulkEmailForm);
|
||||
|
||||
@@ -2,13 +2,15 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
import { Form } from '@openedx/paragon';
|
||||
import useMobileResponsive from '../../../utils/useMobileResponsive';
|
||||
|
||||
function ScheduleEmailForm(props) {
|
||||
const isMobile = useMobileResponsive();
|
||||
const { isValid, onDateTimeChange, dateTime } = props;
|
||||
const { date, time } = dateTime;
|
||||
const descriptionDate = new Date();
|
||||
descriptionDate.setDate(new Date().getDate() + 1);
|
||||
return (
|
||||
<Form.Group>
|
||||
<div className={classNames('d-flex', isMobile ? 'flex-column' : 'flex-row', 'my-3')}>
|
||||
@@ -30,7 +32,10 @@ function ScheduleEmailForm(props) {
|
||||
<small className="text-gray-500 x-small">
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.schedule.date.description"
|
||||
defaultMessage="Enter a start date, e.g. 11/27/2023"
|
||||
defaultMessage="Enter a start date, e.g. {date}"
|
||||
values={{
|
||||
date: descriptionDate.toLocaleDateString(),
|
||||
}}
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
@@ -52,7 +57,10 @@ function ScheduleEmailForm(props) {
|
||||
<small className="text-gray-500 x-small">
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.schedule.time.description"
|
||||
defaultMessage="Enter a start time, e.g. 09:00 AM"
|
||||
defaultMessage="Enter a start time, e.g. {time}"
|
||||
values={{
|
||||
time: descriptionDate.toLocaleTimeString([], { timeStyle: 'short' }),
|
||||
}}
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Form } from '@edx/paragon';
|
||||
import { Form } from '@openedx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import './bulkEmailRecepient.scss';
|
||||
@@ -14,7 +14,13 @@ const DEFAULT_GROUPS = {
|
||||
};
|
||||
|
||||
export default function BulkEmailRecipient(props) {
|
||||
const { handleCheckboxes, selectedGroups, additionalCohorts } = props;
|
||||
const {
|
||||
handleCheckboxes,
|
||||
selectedGroups,
|
||||
additionalCohorts,
|
||||
courseModes,
|
||||
} = props;
|
||||
const hasCourseModes = courseModes && courseModes.length > 1;
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
@@ -50,18 +56,24 @@ export default function BulkEmailRecipient(props) {
|
||||
description="A selectable choice from a list of potential email recipients"
|
||||
/>
|
||||
</Form.Checkbox>
|
||||
<Form.Checkbox
|
||||
key="track:verified"
|
||||
value="track:verified"
|
||||
disabled={selectedGroups.find((group) => group === DEFAULT_GROUPS.ALL_LEARNERS)}
|
||||
className="col col-lg-4 col-sm-6 col-12"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.recipients.verified"
|
||||
defaultMessage="Learners in the verified certificate track"
|
||||
description="A selectable choice from a list of potential email recipients"
|
||||
/>
|
||||
</Form.Checkbox>
|
||||
{
|
||||
// additional modes
|
||||
hasCourseModes
|
||||
&& courseModes.map((courseMode) => (
|
||||
<Form.Checkbox
|
||||
key={`track:${courseMode.slug}`}
|
||||
value={`track:${courseMode.slug}`}
|
||||
disabled={selectedGroups.find((group) => group === DEFAULT_GROUPS.ALL_LEARNERS)}
|
||||
className="col col-lg-4 col-sm-6 col-12"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.mode.label"
|
||||
defaultMessage="Learners in the {courseModeName} Track"
|
||||
values={{ courseModeName: courseMode.name }}
|
||||
/>
|
||||
</Form.Checkbox>
|
||||
))
|
||||
}
|
||||
{
|
||||
// additional cohorts
|
||||
additionalCohorts
|
||||
@@ -80,18 +92,6 @@ export default function BulkEmailRecipient(props) {
|
||||
</Form.Checkbox>
|
||||
))
|
||||
}
|
||||
<Form.Checkbox
|
||||
key="track:audit"
|
||||
value="track:audit"
|
||||
disabled={selectedGroups.find((group) => group === DEFAULT_GROUPS.ALL_LEARNERS)}
|
||||
className="col col-lg-4 col-sm-6 col-12"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.recipients.audit"
|
||||
defaultMessage="Learners in the audit track"
|
||||
description="A selectable choice from a list of potential email recipients"
|
||||
/>
|
||||
</Form.Checkbox>
|
||||
<Form.Checkbox
|
||||
key="learners"
|
||||
value="learners"
|
||||
@@ -127,4 +127,10 @@ BulkEmailRecipient.propTypes = {
|
||||
handleCheckboxes: PropTypes.func.isRequired,
|
||||
isValid: PropTypes.bool,
|
||||
additionalCohorts: PropTypes.arrayOf(PropTypes.string),
|
||||
courseModes: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
slug: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
|
||||
|
||||
/**
|
||||
* Generates an array of course mode objects using Rosie Factory.
|
||||
* @returns {Array<Object>} An array of course mode objects with attributes 'slug' and 'name'.
|
||||
*/
|
||||
const courseModeFactory = () => {
|
||||
const AuditModeFactory = Factory.define('AuditModeFactory')
|
||||
.attr('slug', 'audit')
|
||||
.attr('name', 'Audit');
|
||||
|
||||
const VerifiedModeFactory = Factory.define('VerifiedModeFactory')
|
||||
.attr('slug', 'verified')
|
||||
.attr('name', 'Verified Certificate');
|
||||
|
||||
return [
|
||||
AuditModeFactory.build(),
|
||||
VerifiedModeFactory.build(),
|
||||
];
|
||||
};
|
||||
|
||||
export default courseModeFactory;
|
||||
@@ -41,6 +41,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Subject',
|
||||
description: 'Email subject line input label. Meant to have colon or equivilant punctuation.',
|
||||
},
|
||||
bulkEmailFormSubjectTip: {
|
||||
id: 'bulk.email.form.subject.tip',
|
||||
defaultMessage: '(Maximum 128 characters)',
|
||||
description: 'Default Subject tip',
|
||||
},
|
||||
bulkEmailFormSubjectError: {
|
||||
id: 'bulk.email.form.subject.error',
|
||||
defaultMessage: 'A subject is required',
|
||||
|
||||
@@ -12,6 +12,7 @@ import * as bulkEmailFormApi from '../data/api';
|
||||
import { BulkEmailContext, BulkEmailProvider } from '../../bulk-email-context';
|
||||
import { formatDate } from '../../../../utils/formatDateAndTime';
|
||||
import cohortFactory from '../data/__factories__/bulkEmailFormCohort.factory';
|
||||
import courseModeFactory from '../data/__factories__/bulkEmailFormCourseMode.factory';
|
||||
|
||||
jest.mock('../../text-editor/TextEditor');
|
||||
|
||||
@@ -20,12 +21,17 @@ const dispatchMock = jest.fn();
|
||||
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(new Date().getDate() + 1);
|
||||
const courseMode = courseModeFactory();
|
||||
|
||||
function renderBulkEmailForm() {
|
||||
const { cohorts } = cohortFactory.build();
|
||||
return (
|
||||
<BulkEmailProvider>
|
||||
<BulkEmailForm courseId="test" cohorts={cohorts} />
|
||||
<BulkEmailForm
|
||||
courseId="test"
|
||||
cohorts={cohorts}
|
||||
courseModes={courseMode}
|
||||
/>
|
||||
</BulkEmailProvider>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +39,7 @@ function renderBulkEmailForm() {
|
||||
function renderBulkEmailFormContext(value) {
|
||||
return (
|
||||
<BulkEmailContext.Provider value={[value, dispatchMock]}>
|
||||
<BulkEmailForm courseId="test" />
|
||||
<BulkEmailForm courseId="test" courseMode={courseMode} />
|
||||
</BulkEmailContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -96,8 +102,8 @@ describe('bulk-email-form', () => {
|
||||
test('Checking "All Learners" disables each learner group', async () => {
|
||||
render(renderBulkEmailForm());
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: 'All Learners' }));
|
||||
const verifiedLearners = screen.getByRole('checkbox', { name: 'Learners in the verified certificate track' });
|
||||
const auditLearners = screen.getByRole('checkbox', { name: 'Learners in the audit track' });
|
||||
const verifiedLearners = screen.getByRole('checkbox', { name: 'Learners in the Verified Certificate Track' });
|
||||
const auditLearners = screen.getByRole('checkbox', { name: 'Learners in the Audit Track' });
|
||||
const { cohorts } = cohortFactory.build();
|
||||
cohorts.forEach(cohort => expect(screen.getByRole('checkbox', { name: `Cohort: ${cohort}` })).toBeDisabled());
|
||||
expect(verifiedLearners).toBeDisabled();
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* eslint-disable react/no-unstable-nested-components */
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import {
|
||||
Button, Collapsible, Icon,
|
||||
} from '@edx/paragon';
|
||||
import { SpinnerSimple } from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon';
|
||||
import { SpinnerSimple } from '@openedx/paragon/icons';
|
||||
import messages from './messages';
|
||||
import { getSentEmailHistory } from './data/api';
|
||||
import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
|
||||
@@ -51,17 +51,15 @@ function BulkEmailContentHistory({ intl }) {
|
||||
* up a level (the `subject` field). We also convert the `sent_to` data to be a String rather than an array to fix a
|
||||
* display bug in the table.
|
||||
*/
|
||||
function transformDataForTable() {
|
||||
let tableData = [];
|
||||
if (emailHistoryData) {
|
||||
tableData = emailHistoryData.map((item) => ({
|
||||
...item,
|
||||
subject: item.email.subject,
|
||||
sent_to: item.sent_to.join(', '),
|
||||
}));
|
||||
}
|
||||
return tableData;
|
||||
}
|
||||
const transformDataForTable = useMemo(() => {
|
||||
const tableData = emailHistoryData?.map((item) => ({
|
||||
...item,
|
||||
subject: item.email.subject,
|
||||
sent_to: item.sent_to.join(', '),
|
||||
created: new Date(item.created).toLocaleString(),
|
||||
}));
|
||||
return tableData || [];
|
||||
}, [emailHistoryData]);
|
||||
|
||||
/**
|
||||
* This function is responsible for setting the current `messageContent` state data. This will be the contents of a
|
||||
@@ -102,7 +100,7 @@ function BulkEmailContentHistory({ intl }) {
|
||||
* contents of a previously sent message.
|
||||
*/
|
||||
const additionalColumns = () => {
|
||||
const tableData = transformDataForTable();
|
||||
const tableData = transformDataForTable;
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -139,7 +137,7 @@ function BulkEmailContentHistory({ intl }) {
|
||||
{showHistoricalEmailContentTable ? (
|
||||
<BulkEmailTaskManagerTable
|
||||
errorRetrievingData={errorRetrievingData}
|
||||
tableData={transformDataForTable()}
|
||||
tableData={transformDataForTable}
|
||||
tableDescription={intl.formatMessage(messages.emailHistoryTableViewMessageInstructions)}
|
||||
alertWarningMessage={intl.formatMessage(messages.noEmailData)}
|
||||
alertErrorMessage={intl.formatMessage(messages.errorFetchingEmailHistoryData)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Alert, DataTable } from '@edx/paragon';
|
||||
import { Alert, DataTable } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Hyperlink, Alert } from '@edx/paragon';
|
||||
import { WarningFilled } from '@edx/paragon/icons';
|
||||
import { Hyperlink, Alert } from '@openedx/paragon';
|
||||
import { WarningFilled } from '@openedx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export default function BulkEmailPendingTasksAlert(props) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Icon, Collapsible } from '@edx/paragon';
|
||||
import { SpinnerSimple } from '@edx/paragon/icons';
|
||||
import { Icon, Collapsible } from '@openedx/paragon';
|
||||
import { SpinnerSimple } from '@openedx/paragon/icons';
|
||||
import { getEmailTaskHistory } from './data/api';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -41,6 +41,14 @@ function BulkEmailTaskHistory({ intl }) {
|
||||
setShowHistoricalTaskContentTable(true);
|
||||
}
|
||||
|
||||
const transformDataForTable = useMemo(() => {
|
||||
const tableData = emailTaskHistoryData?.map((item) => ({
|
||||
...item,
|
||||
created: new Date(item.created).toLocaleString(),
|
||||
}));
|
||||
return tableData || [];
|
||||
}, [emailTaskHistoryData]);
|
||||
|
||||
const tableColumns = [
|
||||
{
|
||||
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskType)}`,
|
||||
@@ -95,7 +103,7 @@ function BulkEmailTaskHistory({ intl }) {
|
||||
{showHistoricalTaskContentTable ? (
|
||||
<BulkEmailTaskManagerTable
|
||||
errorRetrievingData={errorRetrievingData}
|
||||
tableData={emailTaskHistoryData}
|
||||
tableData={transformDataForTable}
|
||||
alertWarningMessage={intl.formatMessage(messages.noTaskHistoryData)}
|
||||
alertErrorMessage={intl.formatMessage(messages.errorFetchingTaskHistoryData)}
|
||||
columns={tableColumns}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal } from '@edx/paragon';
|
||||
import { ActionRow, Button, ModalDialog } from '@openedx/paragon';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
import { BulkEmailContext } from '../bulk-email-context';
|
||||
@@ -12,10 +12,13 @@ function ViewEmailModal({
|
||||
const [, dispatch] = useContext(BulkEmailContext);
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
open={isOpen}
|
||||
title=""
|
||||
body={(
|
||||
<ModalDialog
|
||||
isOpen={isOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
hasCloseButton
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
|
||||
<div>
|
||||
<div className="d-flex flex-row">
|
||||
<p>{intl.formatMessage(messages.modalMessageSubject)}</p>
|
||||
@@ -40,24 +43,30 @@ function ViewEmailModal({
|
||||
<div dangerouslySetInnerHTML={{ __html: messageContent.email.html_message }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
onClose={() => setModalOpen(false)}
|
||||
buttons={[
|
||||
<Button
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
copyToEditor({
|
||||
emailBody: messageContent.email.html_message,
|
||||
emailSubject: messageContent.subject,
|
||||
}),
|
||||
);
|
||||
setModalOpen(false);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="bulk.email.tool.copy.message.button" defaultMessage="Copy to editor" />
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="link">
|
||||
<FormattedMessage id="bulk.email.tool.close.modalDialog.button" defaultMessage="Close" />
|
||||
</ModalDialog.CloseButton>
|
||||
<Button
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
copyToEditor({
|
||||
emailBody: messageContent.email.html_message,
|
||||
emailSubject: messageContent.subject,
|
||||
}),
|
||||
);
|
||||
setModalOpen(false);
|
||||
}}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage id="bulk.email.tool.copy.message.button" defaultMessage="Copy to editor" />
|
||||
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import React, {
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert, DataTable, Icon, IconButton, useToggle,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import {
|
||||
Delete, Info, Visibility, Edit,
|
||||
} from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon/icons';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { BulkEmailContext } from '../../bulk-email-context';
|
||||
import { deleteScheduledEmailThunk, getScheduledBulkEmailThunk } from './data/thunks';
|
||||
@@ -26,6 +26,7 @@ function flattenScheduledEmailsArray(emails) {
|
||||
emailId: email.courseEmail.id,
|
||||
task: email.task,
|
||||
taskDue: new Date(email.taskDue).toLocaleString(),
|
||||
taskDueUTC: email.taskDue,
|
||||
...email.courseEmail,
|
||||
targets: email.courseEmail.targets.join(', '),
|
||||
}));
|
||||
@@ -91,10 +92,10 @@ function BulkEmailScheduledEmailsTable({ intl }) {
|
||||
const handleEditEmail = (row) => {
|
||||
const {
|
||||
original: {
|
||||
htmlMessage: emailBody, subject: emailSubject, taskDue, targets, schedulingId, emailId,
|
||||
htmlMessage: emailBody, subject: emailSubject, taskDueUTC, targets, schedulingId, emailId,
|
||||
},
|
||||
} = row;
|
||||
const dateTime = new Date(taskDue);
|
||||
const dateTime = new Date(taskDueUTC);
|
||||
const emailRecipients = targets.replaceAll('-', ':').split(', ');
|
||||
const scheduleDate = formatDate(dateTime);
|
||||
const scheduleTime = formatTime(dateTime);
|
||||
|
||||
@@ -34,6 +34,10 @@ const messages = defineMessages({
|
||||
id: 'bulk.email.content.history.table.modal.messageBody',
|
||||
defaultMessage: 'Message:',
|
||||
},
|
||||
modalCloseButton: {
|
||||
id: 'bulk.email.tool.close.modalDialog.button',
|
||||
defaultMessage: 'Close',
|
||||
},
|
||||
emailHistoryTableViewMessageInstructions: {
|
||||
id: 'bulk.email.content.history.table.viewMessageInstructions',
|
||||
defaultMessage: 'To read a sent email message, click the `View Message` button within the table.',
|
||||
|
||||
@@ -66,7 +66,8 @@ describe('BulkEmailContentHistory component', () => {
|
||||
// verify table contents
|
||||
const { emails } = emailHistoryData;
|
||||
const email = emails[0];
|
||||
expect(await screen.findByText(email.created)).toBeTruthy();
|
||||
const createdDate = new Date(email.created).toLocaleString();
|
||||
expect(await screen.findByText(createdDate)).toBeTruthy();
|
||||
expect(await screen.findByText(email.number_sent)).toBeTruthy();
|
||||
expect(await screen.findByText(email.requester)).toBeTruthy();
|
||||
expect(await screen.findByText(email.sent_to.join(', '))).toBeTruthy();
|
||||
@@ -103,7 +104,8 @@ describe('BulkEmailContentHistory component', () => {
|
||||
expect(await screen.findByText('Message:')).toBeTruthy();
|
||||
expect(await screen.findAllByText(email.email.subject)).toBeTruthy();
|
||||
expect(await screen.findAllByText(email.requester)).toBeTruthy();
|
||||
expect(await screen.findAllByText(email.created)).toBeTruthy();
|
||||
const createdDate = new Date(email.created).toLocaleString();
|
||||
expect(await screen.findAllByText(createdDate)).toBeTruthy();
|
||||
expect(await screen.findAllByText(email.sent_to.join(', '))).toBeTruthy();
|
||||
// .replace() call strips the HTML tags from the string
|
||||
expect(await screen.findByText(email.email.html_message.replace(/<[^>]*>?/gm, ''))).toBeTruthy();
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('BulkEmailPendingTasks component', () => {
|
||||
expect(await screen.findByText('State')).toBeTruthy();
|
||||
expect(await screen.findByText('Status')).toBeTruthy();
|
||||
expect(await screen.findByText('Task Progress')).toBeTruthy();
|
||||
expect(await screen.findAllByText('Showing 1 of 1.')).toBeTruthy();
|
||||
expect(await screen.findAllByText('Showing 1 - 1 of 1.')).toBeTruthy();
|
||||
|
||||
// verification of table contents
|
||||
const { tasks } = pendingInstructorTaskData;
|
||||
|
||||
@@ -51,11 +51,12 @@ describe('BulkEmailTaskHistory component', () => {
|
||||
expect(await screen.findByText('State')).toBeTruthy();
|
||||
expect(await screen.findByText('Status')).toBeTruthy();
|
||||
expect(await screen.findByText('Task Progress')).toBeTruthy();
|
||||
expect(await screen.findAllByText('Showing 1 of 1.')).toBeTruthy();
|
||||
expect(await screen.findAllByText('Showing 1 - 1 of 1.')).toBeTruthy();
|
||||
// verification of row contents
|
||||
const { tasks } = taskHistoryData;
|
||||
const task = tasks[0];
|
||||
expect(await screen.findByText(task.created)).toBeTruthy();
|
||||
const createdDate = new Date(task.created).toLocaleString();
|
||||
expect(await screen.findByText(createdDate)).toBeTruthy();
|
||||
expect(await screen.findByText(task.duration_sec)).toBeTruthy();
|
||||
expect(await screen.findByText(task.requester)).toBeTruthy();
|
||||
expect(await screen.findByText(task.status)).toBeTruthy();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ActionRow, AlertModal, Button } from '@edx/paragon';
|
||||
import { ActionRow, AlertModal, Button } from '@openedx/paragon';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
function TaskAlertModal(props) {
|
||||
|
||||
@@ -43,6 +43,8 @@ export default function TextEditor(props) {
|
||||
block_unsupported_drop: false,
|
||||
image_advtab: true,
|
||||
name: 'emailBody',
|
||||
relative_urls: false,
|
||||
remove_script_host: false,
|
||||
}}
|
||||
onEditorChange={onChange}
|
||||
value={value}
|
||||
|
||||
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Icon } from '@edx/paragon';
|
||||
import { ArrowBack } from '@edx/paragon/icons';
|
||||
import { Button, Icon } from '@openedx/paragon';
|
||||
import { ArrowBack } from '@openedx/paragon/icons';
|
||||
|
||||
export default function BackToInstructor(props) {
|
||||
const { courseId } = props;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Nav } from '@edx/paragon';
|
||||
import { Nav } from '@openedx/paragon';
|
||||
|
||||
export default function NavigationTabs(props) {
|
||||
const { tabData } = props;
|
||||
|
||||
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { Spinner } from '@edx/paragon';
|
||||
import FooterSlot from '@openedx/frontend-slot-footer';
|
||||
import { Spinner } from '@openedx/paragon';
|
||||
|
||||
import { getCohorts, getCourseHomeCourseMetadata } from './data/api';
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function PageContainer(props) {
|
||||
}
|
||||
|
||||
const {
|
||||
org, number, title, tabs, originalUserIsStaff,
|
||||
org, number, title, tabs, originalUserIsStaff, courseModes,
|
||||
} = metadataResponse;
|
||||
const { cohorts } = cohortsResponse;
|
||||
|
||||
@@ -48,6 +48,7 @@ export default function PageContainer(props) {
|
||||
number,
|
||||
title,
|
||||
originalUserIsStaff,
|
||||
courseModes,
|
||||
tabs: [...tabs],
|
||||
cohorts: cohorts.map(({ name }) => name),
|
||||
});
|
||||
@@ -66,10 +67,12 @@ export default function PageContainer(props) {
|
||||
courseNumber={courseMetadata.number}
|
||||
courseTitle={courseMetadata.title}
|
||||
/>
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
<div className="pb-3 container">
|
||||
<main id="main-content">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
<FooterSlot />
|
||||
</>
|
||||
</CourseMetadataContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,41 +1 @@
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import { messages as paragonMessages } from '@edx/paragon';
|
||||
|
||||
import arMessages from './messages/ar.json';
|
||||
import caMessages from './messages/ca.json';
|
||||
// no need to import en messages-- they are in the defaultMessage field
|
||||
import es419Messages from './messages/es_419.json';
|
||||
import frMessages from './messages/fr.json';
|
||||
import zhcnMessages from './messages/zh_CN.json';
|
||||
import heMessages from './messages/he.json';
|
||||
import idMessages from './messages/id.json';
|
||||
import kokrMessages from './messages/ko_kr.json';
|
||||
import plMessages from './messages/pl.json';
|
||||
import ptbrMessages from './messages/pt_br.json';
|
||||
import ruMessages from './messages/ru.json';
|
||||
import thMessages from './messages/th.json';
|
||||
import ukMessages from './messages/uk.json';
|
||||
|
||||
const appMessages = {
|
||||
ar: arMessages,
|
||||
'es-419': es419Messages,
|
||||
fr: frMessages,
|
||||
'zh-cn': zhcnMessages,
|
||||
ca: caMessages,
|
||||
he: heMessages,
|
||||
id: idMessages,
|
||||
'ko-kr': kokrMessages,
|
||||
pl: plMessages,
|
||||
'pt-br': ptbrMessages,
|
||||
ru: ruMessages,
|
||||
th: thMessages,
|
||||
uk: ukMessages,
|
||||
};
|
||||
|
||||
export default [
|
||||
headerMessages,
|
||||
footerMessages,
|
||||
paragonMessages,
|
||||
appMessages,
|
||||
];
|
||||
export default [];
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -2,12 +2,13 @@ import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
import {
|
||||
APP_INIT_ERROR, APP_READY, subscribe, initialize, mergeConfig,
|
||||
APP_INIT_ERROR, APP_READY, subscribe, initialize, mergeConfig, getConfig,
|
||||
} from '@edx/frontend-platform';
|
||||
import { AppProvider, AuthenticatedPageRoute, ErrorPage } from '@edx/frontend-platform/react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { Switch } from 'react-router-dom';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import messages from './i18n';
|
||||
|
||||
import './index.scss';
|
||||
@@ -17,15 +18,21 @@ import PageContainer from './components/page-container/PageContainer';
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider>
|
||||
<div className="pb-3 container">
|
||||
<Switch>
|
||||
<AuthenticatedPageRoute path="/courses/:courseId/bulk_email">
|
||||
<PageContainer>
|
||||
<BulkEmailTool />
|
||||
</PageContainer>
|
||||
</AuthenticatedPageRoute>
|
||||
</Switch>
|
||||
</div>
|
||||
<Helmet>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/courses/:courseId/bulk_email"
|
||||
element={(
|
||||
<AuthenticatedPageRoute>
|
||||
<PageContainer>
|
||||
<BulkEmailTool />
|
||||
</PageContainer>
|
||||
</AuthenticatedPageRoute>
|
||||
)}
|
||||
/>
|
||||
</Routes>
|
||||
</AppProvider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@edx/paragon/scss/core/core";
|
||||
@import "~@openedx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
|
||||
50
src/plugin-slots/FooterSlot/README.md
Normal file
50
src/plugin-slots/FooterSlot/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Footer Slot
|
||||
|
||||
### Slot ID: `footer_slot`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to replace/modify/hide the footer.
|
||||
|
||||
The implementation of the `FooterSlot` component lives in [the `frontend-slot-footer` repository](https://github.com/openedx/frontend-slot-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: {
|
||||
footer_slot: {
|
||||
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;
|
||||
```
|
||||
BIN
src/plugin-slots/FooterSlot/images/custom_footer.png
Normal file
BIN
src/plugin-slots/FooterSlot/images/custom_footer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
src/plugin-slots/FooterSlot/images/default_footer.png
Normal file
BIN
src/plugin-slots/FooterSlot/images/default_footer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
3
src/plugin-slots/README.md
Normal file
3
src/plugin-slots/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `frontend-app-communications` Plugin Slots
|
||||
|
||||
* [`footer_slot`](./FooterSlot/)
|
||||
@@ -10,6 +10,11 @@ import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { configure as configureAuth, MockAuthService } from '@edx/frontend-platform/auth';
|
||||
import messages from './i18n';
|
||||
|
||||
jest.mock('@edx/frontend-platform/react/hooks', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/react/hooks'),
|
||||
useTrackColorSchemeChoice: jest.fn(),
|
||||
}));
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
@@ -24,6 +29,8 @@ Object.defineProperty(window, 'matchMedia', {
|
||||
})),
|
||||
});
|
||||
|
||||
global.Date.prototype.toLocaleDateString = jest.fn();
|
||||
|
||||
export function initializeMockApp() {
|
||||
mergeConfig({
|
||||
// MICROBA-1505: Remove this when we remove the flag from config
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
const config = createConfig('webpack-dev');
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
const config = createConfig('webpack-prod');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user