Compare commits

..

11 Commits

Author SHA1 Message Date
Ihor Romaniuk
d9cce51d86 fix: date format depends on locale date format (#173) 2023-11-16 07:23:14 -03:00
vladislavkeblysh
925a7392cb feat: fixed lint 2023-11-14 12:37:33 -03:00
vladislavkeblysh
42bea23bd1 feat: fixed layout 2023-11-14 12:37:33 -03:00
Stanislav Lunyachek
833de88e1c fix: Missed favicon in Safari 2023-11-14 12:36:40 -03:00
Omar Al-Ithawi
bd85312ab3 feat: use atlas in make pull_translations on palm (#156)
Changes
-------
 - Bump frontend-platform to bring intl-imports.js script
 - Move all i18n imports into `src/i18n/index.js` so intl-imports.js can
   override it with latest translations
 - Add `atlas` into `make pull_translations` when `OPENEDX_ATLAS_PULL`
   environment variable is set.
 - Fixed lint rules for frontend-platform@4.1.0
 - Mock useTrackColorSchemeChoice to avoid test failures
 - Remove all broken and deprecated Tranisfex use
 - Install openedx-atlas

Refs: [FC-0012 project](https://openedx.atlassian.net/l/cp/XGS0iCcQ) implementing Translation Infrastructure OEP-58.
2023-11-03 16:57:13 -04:00
Adolfo R. Brandes
ca7bc7d359 feat: Runtime config support, take 2
Adds a couple of missing features for proper runtime configuration:

1. Favicon runtime configuration support via react-helmet

2. Placeholder values for APP_ID and MFE_CONFIG_API_URL in the sample
   .env files
2023-06-14 15:33:56 +01:00
Adolfo R. Brandes
b41a336e11 feat: Support runtime configuration
frontend-platform supports runtime configuration since 2.5.0 (see the PR
that introduced it[1], but it requires MFE cooperation.  This implements
just that: by avoiding making configuration values constant, it should
now be possible to change them after initialization.

Only a single change related to the `LMS_BASE_URL` setting was required.

[1] openedx/frontend-platform#335
2023-06-13 21:27:27 +01:00
Tobias Macey
b4032215c6 fix: Disable URL rewriting when creating links
The default behavior of the TinyMCE editor is to rewrite links that share the same
domain as the component to be relative to that path. Relative URLs will never work in
email contents, so they _always_ need to be absolute URLs. This adds the configuration
settings for `relative_urls` and `remove_script_host` in TinyMCE to always be false,
enabling it to always use absolute URLs. See
[here](https://www.tiny.cloud/docs/configure/url-handling/) for reference.
2023-06-13 13:08:14 +01:00
Ghassan Maslamani
b20eb50699 test: course url when public path is set
The commit add two tests for the following componenets:

  1. BackToInstructor
  2. BulkEmailPendingTasksAlert

  Which tests course url when public path is set to something
  other than '/' and also when it is '/'.
2023-06-06 16:55:14 +01:00
Ghassan Maslamani
5c021cdc80 fix: getting course-id when public path is set, closes #126
This change change the way course-id is retrieved, in
  1. BackToInstructor
  2. BulkEmailPendingTasksAlert
  componenets, before it was resolved by guessing course-id
  index in the url, which would not be true if the public
  path is set something other than '/'.
  Since public path would shift the index of course-id
  in the url.

  Instead the course-id is resolved through react-router just
  like the container componenet, using the `useParams` hook.
2023-06-06 16:55:14 +01:00
Mashal Malik
fa826fe687 feat: upgrade to node v18 and related fixes (#123) 2023-06-02 12:17:47 +01:00
17 changed files with 7338 additions and 3598 deletions

View File

@@ -10,4 +10,4 @@ on:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

View File

@@ -1,12 +0,0 @@
name: Update Browserslist DB
on:
schedule:
- cron: '0 0 * * 1'
workflow_dispatch:
jobs:
update-browserslist:
uses: openedx/.github/.github/workflows/update-browserslist-db.yml@master
secrets:
requirements_bot_github_token: ${{ secrets.requirements_bot_github_token }}

View File

@@ -1,48 +1,45 @@
|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
#############################
==============================
|license-badge| |status-badge| |ci-badge| |codecov-badge|
Please tag **edx-aperture** on any PRs or issues. Thanks!
Introduction
------------
Purpose
*******
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.
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.
Cloning and Startup
===================
1. Clone the repo into your usual workspace
1. Clone your new repo:
.. code-block::
``git clone https://github.com/edx/frontend-app-communications.git``
mkdir -p ~/workspace/
cd ~/workspace/
git clone https://github.com/edx/frontend-app-communications.git
2. Use node v18.x.
2. Install frontend dependencies
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>`_.
.. code-block::
3. Install npm dependencies:
npm i
``cd frontend-app-communications && npm install``
3. Start the devserver. The app will be running at ``localhost:1984``, or whatever port you change it too.
4. Update the application port to use for local development:
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
.. code-block::
npm start
Environment Variables/Setup Notes
---------------------------------
@@ -68,96 +65,3 @@ 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

View File

@@ -1,18 +0,0 @@
# This file records information about this repo. Its use is described in OEP-55:
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: "frontend-app-communications"
description: "A tool used by course teams to communicate with their learners."
links:
- url: "https://github.com/openedx/frontend-app-communications/blob/master/README.rst"
title: "README"
icon: "Article"
annotations:
openedx.org/arch-interest-groups: ""
spec:
owner: group:frontend-all
type: "service"
lifecycle: "production"

10514
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,11 +34,11 @@
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-footer": "12.2.0",
"@edx/frontend-component-header": "4.6.0",
"@edx/frontend-platform": "5.5.2",
"@edx/frontend-component-footer": "^12.0.0",
"@edx/frontend-component-header": "^4.0.0",
"@edx/frontend-platform": "^4.2.0",
"@edx/openedx-atlas": "^0.5.0",
"@edx/paragon": "^20.44.0",
"@edx/paragon": "^20.20.0",
"@edx/tinymce-language-selector": "1.1.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
@@ -52,12 +52,12 @@
"jquery": "3.6.1",
"popper.js": "1.16.1",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-helmet": "^6.1.0",
"react-redux": "7.2.9",
"react-router": "6.15.0",
"react-router-dom": "6.15.0",
"react-router": "5.3.4",
"react-router-dom": "5.3.4",
"redux": "4.2.0",
"regenerator-runtime": "0.13.11",
"tinymce": "5.10.7"

View File

@@ -33,11 +33,7 @@ export default function BulkEmailTool() {
</h1>
</div>
<div className="row">
<BulkEmailForm
courseId={courseId}
cohorts={courseMetadata.cohorts}
courseModes={courseMetadata.courseModes}
/>
<BulkEmailForm courseId={courseId} cohorts={courseMetadata.cohorts} />
</div>
<div className="row py-5">
<BulkEmailTaskManager courseId={courseId} />

View File

@@ -47,12 +47,7 @@ const FORM_ACTIONS = {
};
function BulkEmailForm(props) {
const {
courseId,
cohorts,
courseModes,
intl,
} = props;
const { courseId, cohorts, intl } = props;
const [{ editor }, dispatch] = useContext(BulkEmailContext);
const [emailFormStatus, setEmailFormStatus] = useState(FORM_SUBMIT_STATES.DEFAULT);
const [emailFormValidation, setEmailFormValidation] = useState({
@@ -277,14 +272,10 @@ 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} maxLength={128} />
<Form.Control.Feedback className="px-3" type="default">
{intl.formatMessage(messages.bulkEmailFormSubjectTip)}
</Form.Control.Feedback>
<Form.Control name="emailSubject" className="w-lg-50" onChange={onFormChange} value={editor.emailSubject} />
{!emailFormValidation.subject && (
<Form.Control.Feedback className="px-3" hasIcon type="invalid">
{intl.formatMessage(messages.bulkEmailFormSubjectError)}
@@ -393,12 +384,6 @@ 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);

View File

@@ -14,13 +14,7 @@ const DEFAULT_GROUPS = {
};
export default function BulkEmailRecipient(props) {
const {
handleCheckboxes,
selectedGroups,
additionalCohorts,
courseModes,
} = props;
const hasCourseModes = courseModes && courseModes.length > 1;
const { handleCheckboxes, selectedGroups, additionalCohorts } = props;
return (
<Form.Group>
<Form.Label>
@@ -56,24 +50,18 @@ export default function BulkEmailRecipient(props) {
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>
))
}
<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 cohorts
additionalCohorts
@@ -92,6 +80,18 @@ 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,10 +127,4 @@ BulkEmailRecipient.propTypes = {
handleCheckboxes: PropTypes.func.isRequired,
isValid: PropTypes.bool,
additionalCohorts: PropTypes.arrayOf(PropTypes.string),
courseModes: PropTypes.arrayOf(
PropTypes.shape({
slug: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}),
).isRequired,
};

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ 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');
@@ -21,17 +20,12 @@ 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}
courseModes={courseMode}
/>
<BulkEmailForm courseId="test" cohorts={cohorts} />
</BulkEmailProvider>
);
}
@@ -39,7 +33,7 @@ function renderBulkEmailForm() {
function renderBulkEmailFormContext(value) {
return (
<BulkEmailContext.Provider value={[value, dispatchMock]}>
<BulkEmailForm courseId="test" courseMode={courseMode} />
<BulkEmailForm courseId="test" />
</BulkEmailContext.Provider>
);
}
@@ -102,8 +96,8 @@ describe('bulk-email-form', () => {
test('Checking "All Learners" disables each learner group', async () => {
render(renderBulkEmailForm());
fireEvent.click(screen.getByRole('checkbox', { name: 'All Learners' }));
const verifiedLearners = screen.getByRole('checkbox', { name: 'Learners in the Verified Certificate Track' });
const auditLearners = screen.getByRole('checkbox', { name: 'Learners in the Audit Track' });
const verifiedLearners = screen.getByRole('checkbox', { name: 'Learners in the verified certificate track' });
const auditLearners = screen.getByRole('checkbox', { name: 'Learners in the audit track' });
const { cohorts } = cohortFactory.build();
cohorts.forEach(cohort => expect(screen.getByRole('checkbox', { name: `Cohort: ${cohort}` })).toBeDisabled());
expect(verifiedLearners).toBeDisabled();

View File

@@ -1,6 +1,6 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { ActionRow, Button, ModalDialog } from '@edx/paragon';
import { Button, Modal } from '@edx/paragon';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
import { BulkEmailContext } from '../bulk-email-context';
@@ -12,13 +12,10 @@ function ViewEmailModal({
const [, dispatch] = useContext(BulkEmailContext);
return (
<div>
<ModalDialog
isOpen={isOpen}
onClose={() => setModalOpen(false)}
hasCloseButton
>
<ModalDialog.Body>
<Modal
open={isOpen}
title=""
body={(
<div>
<div className="d-flex flex-row">
<p>{intl.formatMessage(messages.modalMessageSubject)}</p>
@@ -43,30 +40,24 @@ function ViewEmailModal({
<div dangerouslySetInnerHTML={{ __html: messageContent.email.html_message }} />
</div>
</div>
</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>
)}
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>,
]}
/>
</div>
);
}

View File

@@ -34,10 +34,6 @@ 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.',

View File

@@ -39,7 +39,7 @@ export default function PageContainer(props) {
}
const {
org, number, title, tabs, originalUserIsStaff, courseModes,
org, number, title, tabs, originalUserIsStaff,
} = metadataResponse;
const { cohorts } = cohortsResponse;
@@ -48,7 +48,6 @@ export default function PageContainer(props) {
number,
title,
originalUserIsStaff,
courseModes,
tabs: [...tabs],
cohorts: cohorts.map(({ name }) => name),
});
@@ -60,20 +59,18 @@ export default function PageContainer(props) {
if (courseMetadata) {
return (
<CourseMetadataContext.Provider value={courseMetadata}>
<>
<Header
className="learning-header"
courseOrg={courseMetadata.org}
courseNumber={courseMetadata.number}
courseTitle={courseMetadata.title}
/>
<div className="pb-3 container">
<main>
{children}
</main>
</div>
<Footer />
</>
<Header
className="learning-header"
courseOrg={courseMetadata.org}
courseNumber={courseMetadata.number}
courseTitle={courseMetadata.title}
/>
<div className="pb-3 container">
<main>
{children}
</main>
</div>
<Footer />
</CourseMetadataContext.Provider>
);
}

View File

@@ -8,7 +8,7 @@ import { AppProvider, AuthenticatedPageRoute, ErrorPage } from '@edx/frontend-pl
import ReactDOM from 'react-dom';
import { Helmet } from 'react-helmet';
import { Routes, Route } from 'react-router-dom';
import { Switch } from 'react-router-dom';
import messages from './i18n';
import './index.scss';
@@ -21,18 +21,13 @@ subscribe(APP_READY, () => {
<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>
<Switch>
<AuthenticatedPageRoute path="/courses/:courseId/bulk_email">
<PageContainer>
<BulkEmailTool />
</PageContainer>
</AuthenticatedPageRoute>
</Switch>
</AppProvider>,
document.getElementById('root'),
);

View File

@@ -14,7 +14,6 @@ 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) => ({