Merge branch 'master' of github.com:openedx/frontend-app-communications into Ali-Abbas/react-router-upgrade
This commit is contained in:
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/lockfileversion-check-v3.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 }}
|
||||
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"
|
||||
36193
package-lock.json
generated
36193
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -34,10 +34,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "11.7.3",
|
||||
"@edx/frontend-component-header": "3.7.3",
|
||||
"@edx/frontend-platform": "4.2.0",
|
||||
"@edx/paragon": "^20.28.0",
|
||||
"@edx/frontend-component-footer": "12.1.0",
|
||||
"@edx/frontend-component-header": "4.3.0",
|
||||
"@edx/frontend-platform": "5.0.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,18 +51,19 @@
|
||||
"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": "^6.0.0",
|
||||
"react-router-dom": "6.14.2",
|
||||
"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",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<title>Communications | <%= process.env.SITE_NAME %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
@@ -2,11 +2,12 @@ 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 { Helmet } from 'react-helmet';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import messages from './i18n';
|
||||
|
||||
@@ -17,6 +18,9 @@ import PageContainer from './components/page-container/PageContainer';
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider>
|
||||
<Helmet>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
<div className="pb-3 container">
|
||||
<Routes>
|
||||
<Route
|
||||
|
||||
Reference in New Issue
Block a user