Compare commits
29 Commits
mashal-m/c
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39c18ac4fa | ||
|
|
a4b7b5f28d | ||
|
|
42797240df | ||
|
|
ce1e7d040e | ||
|
|
3e7ea6a3e5 | ||
|
|
7ce2a61233 | ||
|
|
2de12997ce | ||
|
|
f6c8cfa906 | ||
|
|
db181a7945 | ||
|
|
840449f71b | ||
|
|
01f1793414 | ||
|
|
ce45c8012d | ||
|
|
7224785682 | ||
|
|
6d27b7082d | ||
|
|
c90bdb1ac7 | ||
|
|
090968e2f2 | ||
|
|
765857f380 | ||
|
|
0eec29a458 | ||
|
|
c2b4d04b5d | ||
|
|
a198557e67 | ||
|
|
4493eb71fa | ||
|
|
559a1061b6 | ||
|
|
53fa594207 | ||
|
|
0c34d86610 | ||
|
|
05d16cf231 | ||
|
|
55a0ddb8d2 | ||
|
|
344b68e10e | ||
|
|
631d47b286 | ||
|
|
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,3 +1,9 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint');
|
||||
module.exports = createConfig('eslint', {
|
||||
rules: {
|
||||
'react/function-component-definition': 'off',
|
||||
},
|
||||
});
|
||||
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -9,18 +9,18 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Validate package-lock.json changes
|
||||
|
||||
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.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
||||
|
||||
12
.github/workflows/update-browserslist-db.yml
vendored
Normal file
12
.github/workflows/update-browserslist-db.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
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 }}
|
||||
29
Makefile
29
Makefile
@@ -1,11 +1,7 @@
|
||||
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
|
||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
@@ -33,20 +29,17 @@ 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/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull \
|
||||
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
|
||||
|
||||
$(intl_imports) frontend-component-header frontend-component-footer paragon frontend-app-communications
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
142
README.rst
142
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,96 @@ Tests use `jest` and `react-test-library`. To run all the tests for this repo:
|
||||
.. code-block::
|
||||
|
||||
npm test
|
||||
|
||||
|
||||
**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:frontend-all
|
||||
type: "service"
|
||||
lifecycle: "production"
|
||||
38528
package-lock.json
generated
38528
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -34,10 +34,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "11.6.0",
|
||||
"@edx/frontend-component-header": "3.5.0",
|
||||
"@edx/frontend-platform": "2.6.2",
|
||||
"@edx/paragon": "^20.20.0",
|
||||
"@edx/frontend-component-footer": "12.2.0",
|
||||
"@edx/frontend-component-header": "4.6.0",
|
||||
"@edx/frontend-platform": "5.5.2",
|
||||
"@edx/openedx-atlas": "^0.5.0",
|
||||
"@edx/paragon": "^20.44.0",
|
||||
"@edx/tinymce-language-selector": "1.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
@@ -51,27 +52,27 @@
|
||||
"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.1.0",
|
||||
"@edx/frontend-build": "11.0.2",
|
||||
"@edx/browserslist-config": "^1.2.0",
|
||||
"@edx/frontend-build": "^12.7.0",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"axios-mock-adapter": "1.21.2",
|
||||
"codecov": "3.8.3",
|
||||
"glob": "7.2.3",
|
||||
"husky": "7.0.4",
|
||||
"jest": "27.5.1",
|
||||
"prettier": "2.8.1",
|
||||
"reactifex": "1.1.1",
|
||||
"rosie": "2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function BulkEmailTool() {
|
||||
<NavigationTabs courseId={courseId} tabData={courseMetadata.tabs} />
|
||||
<BulkEmailProvider>
|
||||
<Container size="md">
|
||||
<BackToInstructor />
|
||||
<BackToInstructor courseId={courseId} />
|
||||
<div className="row pb-4.5">
|
||||
<h1 className="text-primary-500" id="main-content">
|
||||
<FormattedMessage
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable react/jsx-no-constructed-context-values */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import useAsyncReducer, { combineReducers } from '../../../utils/useAsyncReducer';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/no-unstable-nested-components */
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
|
||||
@@ -9,6 +9,8 @@ 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 +1,2 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './BulkEmailRecipient';
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './BulkEmailForm';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable react/no-unstable-nested-components */
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useParams } from 'react-router-dom';
|
||||
@@ -131,6 +133,7 @@ function BulkEmailContentHistory({ intl }) {
|
||||
styling="card"
|
||||
title={intl.formatMessage(messages.emailHistoryTableSectionButton)}
|
||||
className="mb-3"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onOpen={fetchSentEmailHistoryData}
|
||||
>
|
||||
{showHistoricalEmailContentTable ? (
|
||||
|
||||
@@ -71,12 +71,12 @@ export default function BulkEmailTaskManagerTable(props) {
|
||||
|
||||
BulkEmailTaskManagerTable.propTypes = {
|
||||
errorRetrievingData: PropTypes.bool.isRequired,
|
||||
tableData: PropTypes.arrayOf(PropTypes.object),
|
||||
tableData: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
tableDescription: PropTypes.string,
|
||||
alertWarningMessage: PropTypes.string.isRequired,
|
||||
alertErrorMessage: PropTypes.string.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
additionalColumns: PropTypes.arrayOf(PropTypes.object),
|
||||
columns: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
additionalColumns: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
};
|
||||
|
||||
BulkEmailTaskManagerTable.defaultProps = {
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
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 { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export default function BulkEmailPendingTasksAlert() {
|
||||
export default function BulkEmailPendingTasksAlert(props) {
|
||||
const { courseId } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alert variant="warning" icon={WarningFilled}>
|
||||
<Alert variant="warning" icon={WarningFilled}>
|
||||
<FormattedMessage
|
||||
id="bulk.email.pending.tasks.description.one"
|
||||
defaultMessage="To view all pending tasks, including email, visit "
|
||||
/>
|
||||
<Hyperlink
|
||||
destination={`${getConfig().LMS_BASE_URL}/courses/${courseId}/instructor#view-course-info`}
|
||||
target="_blank"
|
||||
isInline
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="bulk.email.pending.tasks.description.one"
|
||||
defaultMessage="To view all pending tasks, including email, visit "
|
||||
id="bulk.email.pending.tasks.link"
|
||||
defaultMessage="Course Info"
|
||||
/>
|
||||
<Hyperlink
|
||||
destination={`${getConfig().LMS_BASE_URL}/courses/${window.location.pathname.split('/')[2]}/instructor#view-course-info`}
|
||||
target="_blank"
|
||||
isInline
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="bulk.email.pending.tasks.link"
|
||||
defaultMessage="Course Info"
|
||||
/>
|
||||
</Hyperlink>
|
||||
<FormattedMessage
|
||||
id="bulk.email.pending.tasks.description.two"
|
||||
defaultMessage=" in the Instructor Dashboard."
|
||||
/>
|
||||
</Alert>
|
||||
</>
|
||||
</Hyperlink>
|
||||
<FormattedMessage
|
||||
id="bulk.email.pending.tasks.description.two"
|
||||
defaultMessage=" in the Instructor Dashboard."
|
||||
/>
|
||||
</Alert>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
BulkEmailPendingTasksAlert.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
@@ -89,6 +89,7 @@ function BulkEmailTaskHistory({ intl }) {
|
||||
<Collapsible
|
||||
styling="card"
|
||||
title={intl.formatMessage(messages.emailTaskHistoryTableSectionButton)}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onOpen={fetchEmailTaskHistoryData}
|
||||
>
|
||||
{showHistoricalTaskContentTable ? (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
@@ -8,7 +9,7 @@ import messages from './messages';
|
||||
import BulkEmailScheduledEmailsTable from './bulk-email-scheduled-emails-table';
|
||||
import BulkEmailPendingTasksAlert from './BulkEmailPendingTasksAlert';
|
||||
|
||||
function BulkEmailTaskManager({ intl }) {
|
||||
function BulkEmailTaskManager({ intl, courseId }) {
|
||||
return (
|
||||
<div className="w-100">
|
||||
{getConfig().SCHEDULE_EMAIL_SECTION && (
|
||||
@@ -26,7 +27,7 @@ function BulkEmailTaskManager({ intl }) {
|
||||
</div>
|
||||
<div className="border-top border-primary-500 pt-4.5">
|
||||
<h2 className="h3 mb-4 text-primary-500">{intl.formatMessage(messages.pendingTasksHeader)}</h2>
|
||||
<BulkEmailPendingTasksAlert />
|
||||
<BulkEmailPendingTasksAlert courseId={courseId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -34,6 +35,7 @@ function BulkEmailTaskManager({ intl }) {
|
||||
|
||||
BulkEmailTaskManager.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(BulkEmailTaskManager);
|
||||
|
||||
@@ -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 '@edx/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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable react/no-unstable-nested-components */
|
||||
|
||||
import React, {
|
||||
useCallback, useContext, useState, useEffect,
|
||||
} from 'react';
|
||||
@@ -24,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(', '),
|
||||
}));
|
||||
@@ -89,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);
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './BulkEmailScheduledEmailsTable';
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
import BulkEmailPendingTasksAlert from '../BulkEmailPendingTasksAlert';
|
||||
import {
|
||||
initializeMockApp, render, screen,
|
||||
} from '../../../../setupTest';
|
||||
|
||||
describe('Testing BulkEmailPendingTasksAlert Component', () => {
|
||||
beforeAll(async () => {
|
||||
await initializeMockApp();
|
||||
});
|
||||
|
||||
test('Render without Public path', async () => {
|
||||
render(<BulkEmailPendingTasksAlert courseId="test-course-id" />);
|
||||
|
||||
const linkEl = await screen.findByText('Course Info');
|
||||
expect(linkEl.href).toEqual('http://localhost:18000/courses/test-course-id/instructor#view-course-info');
|
||||
});
|
||||
|
||||
test('Render with Public path', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
get() {
|
||||
return { pathname: '/communications/courses/test-course-id/bulk-email' };
|
||||
},
|
||||
});
|
||||
|
||||
render(<BulkEmailPendingTasksAlert courseId="test-course-id" />);
|
||||
|
||||
const linkEl = await screen.findByText('Course Info');
|
||||
expect(linkEl.href).toEqual('http://localhost:18000/courses/test-course-id/instructor#view-course-info');
|
||||
expect(window.location.pathname).toEqual('/communications/courses/test-course-id/bulk-email');
|
||||
});
|
||||
});
|
||||
@@ -1 +1,2 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './BulkEmailTool';
|
||||
|
||||
@@ -40,6 +40,7 @@ function TaskAlertModal(props) {
|
||||
// causing strange click event target issues in safari. To solve this, we want to
|
||||
// wrap the string in a fragment instead of a span, so that the whole button considered
|
||||
// a "button" target, and not a "span inside a button"
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
msg => <>{msg}</>
|
||||
}
|
||||
</FormattedMessage>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './TaskAlertModal';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './TextEditor';
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import React from 'react';
|
||||
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';
|
||||
|
||||
export default function BackToInstructor() {
|
||||
export default function BackToInstructor(props) {
|
||||
const { courseId } = props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
className="mb-4.5 ml-n4.5 text-primary-500"
|
||||
href={`${getConfig().LMS_BASE_URL}/courses/${window.location.pathname.split('/')[2]}/instructor#view-course-info`}
|
||||
href={`${getConfig().LMS_BASE_URL}/courses/${courseId}/instructor#view-course-info`}
|
||||
>
|
||||
<Icon
|
||||
src={ArrowBack}
|
||||
@@ -24,3 +27,7 @@ export default function BackToInstructor() {
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
BackToInstructor.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
33
src/components/navigation-tabs/BackToInstructor.test.jsx
Normal file
33
src/components/navigation-tabs/BackToInstructor.test.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
import BackToInstructor from './BackToInstructor';
|
||||
import {
|
||||
initializeMockApp, render, screen,
|
||||
} from '../../setupTest';
|
||||
|
||||
describe('Testing BackToInstructor Component', () => {
|
||||
beforeAll(async () => {
|
||||
await initializeMockApp();
|
||||
});
|
||||
|
||||
test('Render without Public path', async () => {
|
||||
render(<BackToInstructor courseId="test-course-id" />);
|
||||
|
||||
const linkEl = await screen.findByText('Back to Instructor Dashboard');
|
||||
expect(linkEl.href).toEqual('http://localhost:18000/courses/test-course-id/instructor#view-course-info');
|
||||
});
|
||||
|
||||
test('Render with Public path', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
get() {
|
||||
return { pathname: '/communications/courses/test-course-id/bulk-email' };
|
||||
},
|
||||
});
|
||||
|
||||
render(<BackToInstructor courseId="test-course-id" />);
|
||||
|
||||
const linkEl = await screen.findByText('Back to Instructor Dashboard');
|
||||
expect(linkEl.href).toEqual('http://localhost:18000/courses/test-course-id/instructor#view-course-info');
|
||||
expect(window.location.pathname).toEqual('/communications/courses/test-course-id/bulk-email');
|
||||
});
|
||||
});
|
||||
@@ -66,9 +66,11 @@ export default function PageContainer(props) {
|
||||
courseNumber={courseMetadata.number}
|
||||
courseTitle={courseMetadata.title}
|
||||
/>
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<div className="pb-3 container">
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
</CourseMetadataContext.Provider>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
const courseHomeBaseUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata`;
|
||||
export const getCourseHomeBaseUrl = () => `${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata`;
|
||||
|
||||
export async function getCourseHomeCourseMetadata(courseId) {
|
||||
const courseHomeMetadataUrl = `${courseHomeBaseUrl}/${courseId}`;
|
||||
const courseHomeMetadataUrl = `${getCourseHomeBaseUrl()}/${courseId}`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(courseHomeMetadataUrl);
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
24
src/components/page-container/data/api.test.js
Normal file
24
src/components/page-container/data/api.test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { initializeMockApp } from '../../../setupTest';
|
||||
import * as api from './api';
|
||||
import './__factories__/courseMetadata.factory';
|
||||
|
||||
describe('api', () => {
|
||||
beforeAll(async () => {
|
||||
await initializeMockApp();
|
||||
});
|
||||
|
||||
test('getCourseHomeCourseMetadata', async () => {
|
||||
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
const courseMetadata = Factory.build('courseMetadata');
|
||||
const { id: courseId } = courseMetadata;
|
||||
axiosMock
|
||||
.onGet(`${api.getCourseHomeBaseUrl()}/${courseId}`)
|
||||
.reply(200, courseMetadata);
|
||||
const data = await api.getCourseHomeCourseMetadata(courseId);
|
||||
expect(data).toEqual(camelCaseObject(courseMetadata));
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,7 @@
|
||||
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
|
||||
@@ -13,7 +17,7 @@ import ruMessages from './messages/ru.json';
|
||||
import thMessages from './messages/th.json';
|
||||
import ukMessages from './messages/uk.json';
|
||||
|
||||
const messages = {
|
||||
const appMessages = {
|
||||
ar: arMessages,
|
||||
'es-419': es419Messages,
|
||||
fr: frMessages,
|
||||
@@ -29,4 +33,9 @@ const messages = {
|
||||
uk: ukMessages,
|
||||
};
|
||||
|
||||
export default messages;
|
||||
export default [
|
||||
headerMessages,
|
||||
footerMessages,
|
||||
paragonMessages,
|
||||
appMessages,
|
||||
];
|
||||
@@ -2,16 +2,14 @@ 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 { 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 { Switch } from 'react-router-dom';
|
||||
import appMessages from './i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import messages from './i18n';
|
||||
|
||||
import './index.scss';
|
||||
import BulkEmailTool from './components/bulk-email-tool';
|
||||
@@ -20,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'),
|
||||
);
|
||||
@@ -50,5 +54,5 @@ initialize({
|
||||
);
|
||||
},
|
||||
},
|
||||
messages: [appMessages, headerMessages, footerMessages, paragonMessages],
|
||||
messages,
|
||||
});
|
||||
|
||||
@@ -8,7 +8,12 @@ import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform
|
||||
import { configure as configureLogging, MockLoggingService } from '@edx/frontend-platform/logging';
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { configure as configureAuth, MockAuthService } from '@edx/frontend-platform/auth';
|
||||
import appMessages from './i18n';
|
||||
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,
|
||||
@@ -43,13 +48,18 @@ export function initializeMockApp() {
|
||||
const i18nService = configureI18n({
|
||||
config: getConfig(),
|
||||
loggingService,
|
||||
messages: [appMessages],
|
||||
messages,
|
||||
});
|
||||
|
||||
const authService = configureAuth(MockAuthService, { config: getConfig(), loggingService });
|
||||
return { loggingService, i18nService, authService };
|
||||
}
|
||||
|
||||
jest.mock('@edx/frontend-platform/react/hooks', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/react/hooks'),
|
||||
useTrackColorSchemeChoice: jest.fn(),
|
||||
}));
|
||||
|
||||
function render(ui, options) {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
function Wrapper({ children }) {
|
||||
|
||||
Reference in New Issue
Block a user