Compare commits

...

46 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
Mashal Malik
344b68e10e Update transifex api from v2 to v3 (#119)
* fix: fix conflicts

* refactor: remove duplicate line

* fix: update lock file
2023-03-24 12:48:12 +05:00
Mashal Malik
631d47b286 fix: remove unused codecov pkg (#120) 2023-03-24 12:45:37 +05:00
Justin Hynes
fd3a49d7c6 Merge pull request #50 from openedx/renovate/npm-ejs-vulnerability
chore(deps): update dependency ejs to 3.1.7 [security]
2023-03-09 13:39:54 -05:00
renovate[bot]
a2b2d55db0 chore(deps): update dependency ejs to 3.1.7 [security]
* ignore lint errors (for now) to prioritize getting an updated version of the Comms MFE out with a compromised dependency
2023-03-09 13:27:42 -05:00
Sarina Canelake
fca2cce77c Merge pull request #118 from openedx/repo_checks/ensure_workflows
Update standard workflow files.
2023-02-28 09:35:03 -05:00
Jason Wesson
0dc2e65f60 Merge pull request #90 from openedx/renovate/edx-frontend-component-footer-11.x
fix(deps): update dependency @edx/frontend-component-footer to v11.6.0
2023-02-24 14:14:00 -08:00
Jason Wesson
cddc28c34f Merge pull request #108 from openedx/renovate/actions-setup-node-3.x
chore(deps): update actions/setup-node action to v3
2023-02-24 14:06:20 -08:00
Jason Wesson
56ca914fb4 Merge pull request #111 from openedx/renovate/codecov-codecov-action-3.x
chore(deps): update codecov/codecov-action action to v3
2023-02-24 13:45:32 -08:00
Jason Wesson
1ea43e0ad4 Merge pull request #106 from openedx/renovate/actions-checkout-3.x
chore(deps): update actions/checkout action to v3
2023-02-24 13:40:32 -08:00
Feanil Patel
9d25d6e4d0 build: Creating a missing workflow file add-remove-label-on-comment.yml.
The .github/workflows/add-remove-label-on-comment.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-23 11:07:08 -05:00
Feanil Patel
2888cb6662 build: Creating a missing workflow file add-depr-ticket-to-depr-board.yml.
The .github/workflows/add-depr-ticket-to-depr-board.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-23 11:07:08 -05:00
Feanil Patel
418c78d1f3 build: Creating a missing workflow file self-assign-issue.yml.
The .github/workflows/self-assign-issue.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-23 11:07:07 -05:00
Mashal Malik
4231093347 Merge pull request #117 from Mashal-m/mashal-m/major-paragon-version-upgrade
build: major version upgrade of paragon
2022-12-28 11:43:57 +05:00
Muhammad Abdullah Waheed
b5b90272f8 refactor: updated renovate config to auto update minor and patch versions of edx dependencies (#74) 2022-12-20 13:19:32 +05:00
mashal-m
a08d30fbbb build: major version upgrade of paragon 2022-12-20 12:37:21 +05:00
mashal-m
ece65c83ad build: use shared browserslist configuration 2022-12-16 12:38:58 -05:00
renovate[bot]
fd98b4468e chore(deps): update codecov/codecov-action action to v3 2022-12-12 12:27:36 +00:00
renovate[bot]
4a8df3b50e chore(deps): update actions/setup-node action to v3 2022-12-12 12:27:28 +00:00
renovate[bot]
35f755ccf1 chore(deps): update actions/checkout action to v3 2022-12-12 12:27:22 +00:00
renovate[bot]
6b4bd3b534 fix(deps): update dependency @edx/frontend-component-footer to v11.6.0 2022-12-12 12:27:14 +00:00
renovate[bot]
2d9d195936 fix(deps): update dependency tinymce to v5.10.7 2022-12-12 12:22:24 +00:00
renovate[bot]
1082b27647 chore(deps): update dependency prettier to v2.8.1 2022-12-12 09:54:24 +00:00
Tim McCormack
9782cf108f build: Remove community-engineering CODEOWNERS (#112)
Team no longer exists. See <https://github.com/edx/edx-arch-experiments/issues/132>.
2022-12-09 19:41:48 +00:00
renovate[bot]
466fac7e9e fix(deps): update dependency @edx/frontend-component-header to v3.5.0 2022-12-05 11:02:31 +00:00
renovate[bot]
422632c582 chore(deps): update dependency prettier to v2.8.0 2022-11-28 09:19:27 +00:00
renovate[bot]
67b6512288 fix(deps): update dependency regenerator-runtime to v0.13.11 2022-11-21 10:26:48 +00:00
renovate[bot]
13ba06fd2a fix(deps): update dependency @edx/frontend-component-header to v3.4.1 2022-11-15 00:07:05 +00:00
renovate[bot]
61a2a4e8c9 fix(deps): update dependency core-js to v3.26.1 2022-11-14 08:47:19 +00:00
renovate[bot]
e112c3a6d1 fix(deps): update react-router monorepo to v5.3.4 2022-11-07 11:41:03 +00:00
renovate[bot]
97a21b9574 fix(deps): update dependency core-js to v3.26.0 2022-11-07 08:15:48 +00:00
Zubair Shakoor
22675fd17a fix: -t flag added in pull translation command (#100) 2022-10-28 14:54:27 +05:00
renovate[bot]
32327cde93 fix(deps): update dependency @edx/frontend-component-header to v3.3.0 2022-10-24 11:00:17 +00:00
renovate[bot]
dadbfed8e1 fix(deps): update dependency tinymce to v5.10.6 2022-10-24 07:14:55 +00:00
renovate[bot]
262ea5be0d fix(deps): update dependency redux to v4.2.0 2022-10-17 10:57:56 +00:00
renovate[bot]
fcb393d9e7 fix(deps): update dependency regenerator-runtime to v0.13.10 2022-10-17 07:58:39 +00:00
43 changed files with 9581 additions and 33093 deletions

2
.env
View File

@@ -19,3 +19,5 @@ SEGMENT_KEY=''
SITE_NAME=''
USER_INFO_COOKIE_NAME=''
SCHEDULE_EMAIL_SECTION=''
APP_ID=''
MFE_CONFIG_API_URL=''

View File

@@ -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=''

View File

@@ -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=''

View File

@@ -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',
},
});

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @edx/community-engineering

View File

@@ -0,0 +1,19 @@
# Run the workflow that adds new tickets that are either:
# - labelled "DEPR"
# - title starts with "[DEPR]"
# - body starts with "Proposal Date" (this is the first template field)
# to the org-wide DEPR project board
name: Add newly created DEPR issues to the DEPR project board
on:
issues:
types: [opened]
jobs:
routeissue:
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
secrets:
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}

View File

@@ -0,0 +1,20 @@
# This workflow runs when a comment is made on the ticket
# If the comment starts with "label: " it tries to apply
# the label indicated in rest of comment.
# If the comment starts with "remove label: ", it tries
# to remove the indicated label.
# Note: Labels are allowed to have spaces and this script does
# not parse spaces (as often a space is legitimate), so the command
# "label: really long lots of words label" will apply the
# label "really long lots of words label"
name: Allows for the adding and removing of labels via comment
on:
issue_comment:
types: [created]
jobs:
add_remove_labels:
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master

View File

@@ -9,18 +9,18 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [16]
steps:
- name: Checkout
uses: actions/checkout@v2
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@v2
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
@@ -33,7 +33,5 @@ jobs:
run: npm run build
- name: i18n_extract
run: npm run i18n_extract
- name: is-es5
run: npm run is-es5
- name: Coverage
uses: codecov/codecov-action@v2
uses: codecov/codecov-action@v3

View File

@@ -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/self-assign-issue.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# This workflow runs when a comment is made on the ticket
# If the comment starts with "assign me" it assigns the author to the
# ticket (case insensitive)
name: Assign comment author to ticket if they say "assign me"
on:
issue_comment:
types: [created]
jobs:
self_assign_by_comment:
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master

2
.nvmrc
View File

@@ -1,2 +1,2 @@
16
18

View File

@@ -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 -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:

42238
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,13 +7,11 @@
"url": "git+https://github.com/edx/frontend-app-communications.git"
},
"browserslist": [
"last 2 versions",
"ie 11"
"extends @edx/browserslist-config"
],
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"is-es5": "es-check es5 ./dist/*.js",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
@@ -36,10 +34,11 @@
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-footer": "11.2.1",
"@edx/frontend-component-header": "3.2.1",
"@edx/frontend-platform": "2.6.2",
"@edx/paragon": "19.25.3",
"@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.20.0",
"@edx/tinymce-language-selector": "1.1.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
@@ -49,31 +48,31 @@
"@tinymce/tinymce-react": "3.14.0",
"axios": "0.27.2",
"classnames": "2.3.2",
"core-js": "3.25.5",
"core-js": "3.26.1",
"jquery": "3.6.1",
"popper.js": "1.16.1",
"prop-types": "15.8.1",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-helmet": "^6.1.0",
"react-redux": "7.2.9",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"redux": "4.1.2",
"regenerator-runtime": "0.13.9",
"tinymce": "5.10.5"
"react-router": "5.3.4",
"react-router-dom": "5.3.4",
"redux": "4.2.0",
"regenerator-runtime": "0.13.11",
"tinymce": "5.10.7"
},
"devDependencies": {
"@edx/frontend-build": "9.2.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",
"es-check": "6.2.1",
"glob": "7.2.3",
"husky": "7.0.4",
"jest": "27.5.1",
"prettier": "2.7.1",
"reactifex": "1.1.1",
"prettier": "2.8.1",
"rosie": "2.1.0"
}
}

View File

@@ -22,6 +22,11 @@
"pin"
],
"automerge": true
},
{
"matchPackagePatterns": ["@edx"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}
],
"timezone": "America/New_York"

View File

@@ -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

View File

@@ -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';

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/no-unstable-nested-components */
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
@@ -205,6 +206,7 @@ function BulkEmailForm(props) {
} else {
setEmailFormStatus(FORM_SUBMIT_STATES.DEFAULT);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isScheduled, editor.editMode, editor.isLoading, editor.errorRetrievingData, editor.formComplete]);
const AlertMessage = () => (

View File

@@ -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>

View File

@@ -1 +1,2 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './BulkEmailRecipient';

View File

@@ -1 +1,2 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './BulkEmailForm';

View File

@@ -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 ? (

View File

@@ -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 = {

View File

@@ -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&nbsp;"
/>
<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&nbsp;"
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="&nbsp;in the Instructor Dashboard."
/>
</Alert>
</>
</Hyperlink>
<FormattedMessage
id="bulk.email.pending.tasks.description.two"
defaultMessage="&nbsp;in the Instructor Dashboard."
/>
</Alert>
);
}
BulkEmailPendingTasksAlert.propTypes = {
courseId: PropTypes.string.isRequired,
};

View File

@@ -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 ? (

View File

@@ -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);

View File

@@ -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(', '),
}));
@@ -46,6 +49,7 @@ function BulkEmailScheduledEmailsTable({ intl }) {
const fetchTableData = useCallback((args) => {
dispatch(getScheduledBulkEmailThunk(courseId, args.pageIndex + 1));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleViewEmail = (row) => {
@@ -88,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);

View File

@@ -1 +1,2 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './BulkEmailScheduledEmailsTable';

View File

@@ -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');
});
});

View File

@@ -1 +1,2 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './BulkEmailTool';

View File

@@ -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>

View File

@@ -1 +1,2 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './TaskAlertModal';

View File

@@ -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}

View File

@@ -1 +1,2 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './TextEditor';

View File

@@ -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,
};

View 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');
});
});

View File

@@ -53,23 +53,24 @@ export default function PageContainer(props) {
});
}
fetchCourseMetadata();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (courseMetadata) {
return (
<CourseMetadataContext.Provider value={courseMetadata}>
<>
<Header
className="learning-header"
courseOrg={courseMetadata.org}
courseNumber={courseMetadata.number}
courseTitle={courseMetadata.title}
/>
<Header
className="learning-header"
courseOrg={courseMetadata.org}
courseNumber={courseMetadata.number}
courseTitle={courseMetadata.title}
/>
<div className="pb-3 container">
<main>
{children}
</main>
<Footer />
</>
</div>
<Footer />
</CourseMetadataContext.Provider>
);
}

View File

@@ -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);
}

View 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));
});
});

View File

@@ -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,
];

View File

@@ -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 { Helmet } from 'react-helmet';
import { Switch } from 'react-router-dom';
import appMessages from './i18n';
import messages from './i18n';
import './index.scss';
import BulkEmailTool from './components/bulk-email-tool';
@@ -20,15 +18,16 @@ 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>
<Switch>
<AuthenticatedPageRoute path="/courses/:courseId/bulk_email">
<PageContainer>
<BulkEmailTool />
</PageContainer>
</AuthenticatedPageRoute>
</Switch>
</AppProvider>,
document.getElementById('root'),
);
@@ -50,5 +49,5 @@ initialize({
);
},
},
messages: [appMessages, headerMessages, footerMessages],
messages,
});

View File

@@ -8,8 +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,
value: jest.fn().mockImplementation((query) => ({
@@ -43,13 +47,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 }) {

View File

@@ -37,6 +37,7 @@ export default function useMobileResponsive(breakpoint) {
window.addEventListener('resize', checkForMobile);
// return this function here to clean up the event listener
return () => window.removeEventListener('resize', checkForMobile);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return isMobileWindow;
}