Compare commits
6 Commits
repo-tools
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca7bc7d359 | ||
|
|
b41a336e11 | ||
|
|
b4032215c6 | ||
|
|
b20eb50699 | ||
|
|
5c021cdc80 | ||
|
|
fa826fe687 |
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
|
||||
|
||||
28870
package-lock.json
generated
28870
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -34,9 +34,9 @@
|
||||
},
|
||||
"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/frontend-component-footer": "^12.0.0",
|
||||
"@edx/frontend-component-header": "^4.0.0",
|
||||
"@edx/frontend-platform": "^4.0.1",
|
||||
"@edx/paragon": "^20.20.0",
|
||||
"@edx/tinymce-language-selector": "1.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
@@ -53,6 +53,7 @@
|
||||
"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.3.4",
|
||||
"react-router-dom": "5.3.4",
|
||||
@@ -61,8 +62,8 @@
|
||||
"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,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';
|
||||
|
||||
@@ -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,7 +2,7 @@ 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';
|
||||
@@ -10,6 +10,7 @@ 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 { Helmet } from 'react-helmet';
|
||||
import { Switch } from 'react-router-dom';
|
||||
import appMessages from './i18n';
|
||||
|
||||
@@ -20,6 +21,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">
|
||||
<Switch>
|
||||
<AuthenticatedPageRoute path="/courses/:courseId/bulk_email">
|
||||
|
||||
@@ -10,6 +10,10 @@ import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { configure as configureAuth, MockAuthService } from '@edx/frontend-platform/auth';
|
||||
import appMessages 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) => ({
|
||||
|
||||
Reference in New Issue
Block a user