Compare commits
14 Commits
release/ul
...
bilalqamar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f9e520481 | ||
|
|
9eeda23ceb | ||
|
|
fe9d201bd5 | ||
|
|
3586fab6b1 | ||
|
|
8a6cd7937a | ||
|
|
c6563d8ef6 | ||
|
|
f14e80099e | ||
|
|
4977918e65 | ||
|
|
4e2577191d | ||
|
|
b849006b34 | ||
|
|
c3088d986e | ||
|
|
4b7068b49e | ||
|
|
bb28674c1b | ||
|
|
91e4e4b9df |
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint');
|
||||
module.exports = createConfig('eslint');
|
||||
|
||||
28608
package-lock.json
generated
28608
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.1.0",
|
||||
"@edx/frontend-build": "9.2.2",
|
||||
"@edx/frontend-build": "12.3.0",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"axios-mock-adapter": "1.21.2",
|
||||
|
||||
@@ -12,7 +12,7 @@ import { CourseMetadataContext } from '../page-container/PageContainer';
|
||||
import { BulkEmailProvider } from './bulk-email-context';
|
||||
import BackToInstructor from '../navigation-tabs/BackToInstructor';
|
||||
|
||||
export default function BulkEmailTool() {
|
||||
const BulkEmailTool = () => {
|
||||
const { courseId } = useParams();
|
||||
|
||||
return (
|
||||
@@ -46,4 +46,6 @@ export default function BulkEmailTool() {
|
||||
))}
|
||||
</CourseMetadataContext.Consumer>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default BulkEmailTool;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import useAsyncReducer, { combineReducers } from '../../../utils/useAsyncReducer';
|
||||
import editor, { editorInitialState } from '../bulk-email-form/data/reducer';
|
||||
@@ -8,7 +8,7 @@ import scheduledEmailsTable, {
|
||||
|
||||
export const BulkEmailContext = React.createContext();
|
||||
|
||||
export default function BulkEmailProvider({ children }) {
|
||||
const BulkEmailProvider = ({ children }) => {
|
||||
const initialState = {
|
||||
editor: editorInitialState,
|
||||
scheduledEmailsTable: scheduledEmailsTableInitialState,
|
||||
@@ -17,9 +17,14 @@ export default function BulkEmailProvider({ children }) {
|
||||
combineReducers({ editor, scheduledEmailsTable }),
|
||||
initialState,
|
||||
);
|
||||
return <BulkEmailContext.Provider value={[state, dispatch]}>{children}</BulkEmailContext.Provider>;
|
||||
}
|
||||
|
||||
const contextValue = useMemo(() => ([state, dispatch]), [dispatch, state]);
|
||||
|
||||
return <BulkEmailContext.Provider value={contextValue}>{children}</BulkEmailContext.Provider>;
|
||||
};
|
||||
|
||||
BulkEmailProvider.propTypes = {
|
||||
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
|
||||
};
|
||||
|
||||
export default BulkEmailProvider;
|
||||
|
||||
@@ -45,7 +45,52 @@ const FORM_ACTIONS = {
|
||||
PATCH: 'PATCH',
|
||||
};
|
||||
|
||||
function BulkEmailForm(props) {
|
||||
const AlertMessage = (intl, editor, isScheduled) => (
|
||||
<>
|
||||
<p>{intl.formatMessage(messages.bulkEmailTaskAlertRecipients, { subject: editor.emailSubject })}</p>
|
||||
<ul className="list-unstyled">
|
||||
{editor.emailRecipients.map((group) => (
|
||||
<li key={group}>{group}</li>
|
||||
))}
|
||||
</ul>
|
||||
{!isScheduled && (
|
||||
<p>
|
||||
<strong>{intl.formatMessage(messages.bulkEmailInstructionsCaution)}</strong>
|
||||
{intl.formatMessage(messages.bulkEmailInstructionsCautionMessage)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const EditMessage = (intl, editor, isScheduled) => (
|
||||
<>
|
||||
<p>
|
||||
{intl.formatMessage(messages.bulkEmailTaskAlertEditingDate, {
|
||||
dateTime: new Date(`${editor.scheduleDate} ${editor.scheduleTime}`).toLocaleString(),
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(messages.bulkEmailTaskAlertEditingSubject, {
|
||||
subject: editor.emailSubject,
|
||||
})}
|
||||
</p>
|
||||
<p>{intl.formatMessage(messages.bulkEmailTaskAlertEditingTo)}</p>
|
||||
<ul className="list-unstyled">
|
||||
{editor.emailRecipients.map((group) => (
|
||||
<li key={group}>{group}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>{intl.formatMessage(messages.bulkEmailTaskAlertEditingWarning)}</p>
|
||||
{!isScheduled && (
|
||||
<p>
|
||||
<strong>{intl.formatMessage(messages.bulkEmailInstructionsCaution)}</strong>
|
||||
{intl.formatMessage(messages.bulkEmailInstructionsCautionMessage)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const BulkEmailForm = (props) => {
|
||||
const { courseId, cohorts, intl } = props;
|
||||
const [{ editor }, dispatch] = useContext(BulkEmailContext);
|
||||
const [emailFormStatus, setEmailFormStatus] = useState(FORM_SUBMIT_STATES.DEFAULT);
|
||||
@@ -205,58 +250,15 @@ function BulkEmailForm(props) {
|
||||
} else {
|
||||
setEmailFormStatus(FORM_SUBMIT_STATES.DEFAULT);
|
||||
}
|
||||
}, [isScheduled, editor.editMode, editor.isLoading, editor.errorRetrievingData, editor.formComplete]);
|
||||
|
||||
const AlertMessage = () => (
|
||||
<>
|
||||
<p>{intl.formatMessage(messages.bulkEmailTaskAlertRecipients, { subject: editor.emailSubject })}</p>
|
||||
<ul className="list-unstyled">
|
||||
{editor.emailRecipients.map((group) => (
|
||||
<li key={group}>{group}</li>
|
||||
))}
|
||||
</ul>
|
||||
{!isScheduled && (
|
||||
<p>
|
||||
<strong>{intl.formatMessage(messages.bulkEmailInstructionsCaution)}</strong>
|
||||
{intl.formatMessage(messages.bulkEmailInstructionsCautionMessage)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const EditMessage = () => (
|
||||
<>
|
||||
<p>
|
||||
{intl.formatMessage(messages.bulkEmailTaskAlertEditingDate, {
|
||||
dateTime: new Date(`${editor.scheduleDate} ${editor.scheduleTime}`).toLocaleString(),
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(messages.bulkEmailTaskAlertEditingSubject, {
|
||||
subject: editor.emailSubject,
|
||||
})}
|
||||
</p>
|
||||
<p>{intl.formatMessage(messages.bulkEmailTaskAlertEditingTo)}</p>
|
||||
<ul className="list-unstyled">
|
||||
{editor.emailRecipients.map((group) => (
|
||||
<li key={group}>{group}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>{intl.formatMessage(messages.bulkEmailTaskAlertEditingWarning)}</p>
|
||||
{!isScheduled && (
|
||||
<p>
|
||||
<strong>{intl.formatMessage(messages.bulkEmailInstructionsCaution)}</strong>
|
||||
{intl.formatMessage(messages.bulkEmailInstructionsCautionMessage)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [isScheduled, editor.editMode, editor.isLoading, editor.errorRetrievingData,
|
||||
editor.formComplete, delayedEmailFormReset]);
|
||||
|
||||
return (
|
||||
<div className={classNames('w-100 m-auto', !isMobile && 'p-4 border border-primary-200')}>
|
||||
<TaskAlertModal
|
||||
isOpen={isTaskAlertOpen}
|
||||
alertMessage={editor.editMode ? EditMessage() : AlertMessage()}
|
||||
alertMessage={editor.editMode
|
||||
? EditMessage(intl, editor, isScheduled) : AlertMessage(intl, editor, isScheduled)}
|
||||
close={(event) => {
|
||||
closeTaskAlert();
|
||||
if (event.target.name === 'continue') {
|
||||
@@ -372,7 +374,7 @@ function BulkEmailForm(props) {
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
BulkEmailForm.defaultProps = {
|
||||
cohorts: [],
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
import useMobileResponsive from '../../../utils/useMobileResponsive';
|
||||
|
||||
function ScheduleEmailForm(props) {
|
||||
const ScheduleEmailForm = (props) => {
|
||||
const isMobile = useMobileResponsive();
|
||||
const { isValid, onDateTimeChange, dateTime } = props;
|
||||
const { date, time } = dateTime;
|
||||
@@ -68,7 +68,7 @@ function ScheduleEmailForm(props) {
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ScheduleEmailForm.defaultProps = {
|
||||
dateTime: {
|
||||
|
||||
@@ -13,7 +13,7 @@ const DEFAULT_GROUPS = {
|
||||
AUDIT: 'track:audit',
|
||||
};
|
||||
|
||||
export default function BulkEmailRecipient(props) {
|
||||
const BulkEmailRecipient = (props) => {
|
||||
const { handleCheckboxes, selectedGroups, additionalCohorts } = props;
|
||||
return (
|
||||
<Form.Group>
|
||||
@@ -115,7 +115,7 @@ export default function BulkEmailRecipient(props) {
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
BulkEmailRecipient.defaultProps = {
|
||||
isValid: true,
|
||||
@@ -128,3 +128,5 @@ BulkEmailRecipient.propTypes = {
|
||||
isValid: PropTypes.bool,
|
||||
additionalCohorts: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
export default BulkEmailRecipient;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -32,7 +32,7 @@ function renderBulkEmailForm() {
|
||||
|
||||
function renderBulkEmailFormContext(value) {
|
||||
return (
|
||||
<BulkEmailContext.Provider value={[value, dispatchMock]}>
|
||||
<BulkEmailContext.Provider value={value}>
|
||||
<BulkEmailForm courseId="test" />
|
||||
</BulkEmailContext.Provider>
|
||||
);
|
||||
@@ -144,7 +144,7 @@ describe('bulk-email-form', () => {
|
||||
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onPatch().reply(200);
|
||||
render(
|
||||
renderBulkEmailFormContext({
|
||||
renderBulkEmailFormContext([{
|
||||
editor: {
|
||||
editMode: true,
|
||||
emailBody: 'test',
|
||||
@@ -157,7 +157,7 @@ describe('bulk-email-form', () => {
|
||||
isLoading: false,
|
||||
errorRetrievingData: false,
|
||||
},
|
||||
}),
|
||||
}, dispatchMock]),
|
||||
);
|
||||
const submitButton = screen.getByText('Reschedule Email');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { getSentEmailHistory } from './data/api';
|
||||
import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
|
||||
import ViewEmailModal from './ViewEmailModal';
|
||||
|
||||
function BulkEmailContentHistory({ intl }) {
|
||||
const BulkEmailContentHistory = ({ intl }) => {
|
||||
const { courseId } = useParams();
|
||||
const [emailHistoryData, setEmailHistoryData] = useState();
|
||||
const [errorRetrievingData, setErrorRetrievingData] = useState(false);
|
||||
@@ -24,7 +24,7 @@ function BulkEmailContentHistory({ intl }) {
|
||||
* Async function that makes a REST API call to retrieve historical email message data sent by the bulk course email
|
||||
* tool from edx-platform.
|
||||
*/
|
||||
async function fetchSentEmailHistoryData() {
|
||||
const fetchSentEmailHistoryData = async () => {
|
||||
setErrorRetrievingData(false);
|
||||
setShowHistoricalEmailContentTable(false);
|
||||
|
||||
@@ -41,7 +41,7 @@ function BulkEmailContentHistory({ intl }) {
|
||||
}
|
||||
|
||||
setShowHistoricalEmailContentTable(true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This utility function transforms the data stored in `emailHistoryData` to make it easier to display in the Paragon
|
||||
@@ -94,6 +94,13 @@ function BulkEmailContentHistory({ intl }) {
|
||||
},
|
||||
];
|
||||
|
||||
const getViewMessageCell = (tableData, row) => (
|
||||
<Button variant="link" className="px-1" onClick={() => onViewMessageClick(tableData[row.index])}>
|
||||
{intl.formatMessage(messages.buttonViewMessage)}
|
||||
<span className="sr-only"> {row.index}</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
/**
|
||||
* Paragon's DataTable supports the ability to add extra columns that might not directly coincide with the data being
|
||||
* represented in the table. We are using an additional column to embed a button that will open a Modal to display the
|
||||
@@ -106,12 +113,7 @@ function BulkEmailContentHistory({ intl }) {
|
||||
{
|
||||
id: 'view_message',
|
||||
Header: '',
|
||||
Cell: ({ row }) => (
|
||||
<Button variant="link" className="px-1" onClick={() => onViewMessageClick(tableData[row.index])}>
|
||||
{intl.formatMessage(messages.buttonViewMessage)}
|
||||
<span className="sr-only"> {row.index}</span>
|
||||
</Button>
|
||||
),
|
||||
Cell: ({ row }) => getViewMessageCell(tableData, row),
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -150,7 +152,7 @@ function BulkEmailContentHistory({ intl }) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
BulkEmailContentHistory.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Alert, DataTable } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
export default function BulkEmailTaskManagerTable(props) {
|
||||
const BulkEmailTaskManagerTable = (props) => {
|
||||
const {
|
||||
errorRetrievingData,
|
||||
tableData,
|
||||
@@ -67,16 +67,16 @@ export default function BulkEmailTaskManagerTable(props) {
|
||||
{canRenderTable()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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 = {
|
||||
@@ -84,3 +84,5 @@ BulkEmailTaskManagerTable.defaultProps = {
|
||||
tableDescription: '',
|
||||
additionalColumns: [],
|
||||
};
|
||||
|
||||
export default BulkEmailTaskManagerTable;
|
||||
|
||||
@@ -7,7 +7,7 @@ import messages from './messages';
|
||||
import useInterval from '../../../utils/useInterval';
|
||||
import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
|
||||
|
||||
function BulkEmailPendingTasks({ intl }) {
|
||||
const BulkEmailPendingTasks = ({ intl }) => {
|
||||
const { courseId } = useParams();
|
||||
|
||||
const [instructorTaskData, setInstructorTaskData] = useState();
|
||||
@@ -87,7 +87,7 @@ function BulkEmailPendingTasks({ intl }) {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
BulkEmailPendingTasks.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -5,30 +5,28 @@ import { Hyperlink, Alert } from '@edx/paragon';
|
||||
import { WarningFilled } from '@edx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export default function BulkEmailPendingTasksAlert() {
|
||||
return (
|
||||
<>
|
||||
<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/${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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
const BulkEmailPendingTasksAlert = () => (
|
||||
<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/${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>
|
||||
);
|
||||
|
||||
export default BulkEmailPendingTasksAlert;
|
||||
|
||||
@@ -11,7 +11,7 @@ import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
|
||||
|
||||
import './bulkEmailTaskHistory.scss';
|
||||
|
||||
function BulkEmailTaskHistory({ intl }) {
|
||||
const BulkEmailTaskHistory = ({ intl }) => {
|
||||
const { courseId } = useParams();
|
||||
|
||||
const [emailTaskHistoryData, setEmailTaskHistoryData] = useState([]);
|
||||
@@ -22,7 +22,7 @@ function BulkEmailTaskHistory({ intl }) {
|
||||
* Async function that makes a REST API call to retrieve historical bulk email (Instructor) task data for display
|
||||
* within this component.
|
||||
*/
|
||||
async function fetchEmailTaskHistoryData() {
|
||||
const fetchEmailTaskHistoryData = async () => {
|
||||
setErrorRetrievingData(false);
|
||||
setShowHistoricalTaskContentTable(false);
|
||||
|
||||
@@ -39,7 +39,7 @@ function BulkEmailTaskHistory({ intl }) {
|
||||
}
|
||||
|
||||
setShowHistoricalTaskContentTable(true);
|
||||
}
|
||||
};
|
||||
|
||||
const tableColumns = [
|
||||
{
|
||||
@@ -106,7 +106,7 @@ function BulkEmailTaskHistory({ intl }) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
BulkEmailTaskHistory.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -8,29 +8,27 @@ import messages from './messages';
|
||||
import BulkEmailScheduledEmailsTable from './bulk-email-scheduled-emails-table';
|
||||
import BulkEmailPendingTasksAlert from './BulkEmailPendingTasksAlert';
|
||||
|
||||
function BulkEmailTaskManager({ intl }) {
|
||||
return (
|
||||
<div className="w-100">
|
||||
{getConfig().SCHEDULE_EMAIL_SECTION && (
|
||||
<div>
|
||||
<h2 className="h3 text-primary-500">{intl.formatMessage(messages.scheduledEmailsTableHeader)}</h2>
|
||||
<BulkEmailScheduledEmailsTable />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h2 className="h3 text-primary-500">{intl.formatMessage(messages.emailTaskHistoryHeader)}</h2>
|
||||
<BulkEmailContentHistory />
|
||||
</div>
|
||||
<div>
|
||||
<BulkEmailTaskHistory />
|
||||
</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 />
|
||||
</div>
|
||||
const BulkEmailTaskManager = ({ intl }) => (
|
||||
<div className="w-100">
|
||||
{getConfig().SCHEDULE_EMAIL_SECTION && (
|
||||
<div>
|
||||
<h2 className="h3 text-primary-500">{intl.formatMessage(messages.scheduledEmailsTableHeader)}</h2>
|
||||
<BulkEmailScheduledEmailsTable />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
<div>
|
||||
<h2 className="h3 text-primary-500">{intl.formatMessage(messages.emailTaskHistoryHeader)}</h2>
|
||||
<BulkEmailContentHistory />
|
||||
</div>
|
||||
<div>
|
||||
<BulkEmailTaskHistory />
|
||||
</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 />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
BulkEmailTaskManager.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -6,9 +6,9 @@ import messages from './messages';
|
||||
import { BulkEmailContext } from '../bulk-email-context';
|
||||
import { copyToEditor } from '../bulk-email-form/data/actions';
|
||||
|
||||
function ViewEmailModal({
|
||||
const ViewEmailModal = ({
|
||||
intl, messageContent, isOpen, setModalOpen,
|
||||
}) {
|
||||
}) => {
|
||||
const [, dispatch] = useContext(BulkEmailContext);
|
||||
return (
|
||||
<div>
|
||||
@@ -60,7 +60,7 @@ function ViewEmailModal({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ViewEmailModal.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -29,7 +29,7 @@ function flattenScheduledEmailsArray(emails) {
|
||||
}));
|
||||
}
|
||||
|
||||
function BulkEmailScheduledEmailsTable({ intl }) {
|
||||
const BulkEmailScheduledEmailsTable = ({ intl }) => {
|
||||
const { courseId } = useParams();
|
||||
const [{ scheduledEmailsTable }, dispatch] = useContext(BulkEmailContext);
|
||||
const [tableData, setTableData] = useState([]);
|
||||
@@ -46,6 +46,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) => {
|
||||
@@ -108,6 +109,25 @@ function BulkEmailScheduledEmailsTable({ intl }) {
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const getCell = (row, state, page, previousPage) => (
|
||||
<>
|
||||
<IconButton src={Visibility} iconAs={Icon} alt="View" onClick={() => handleViewEmail(row)} />
|
||||
<IconButton
|
||||
src={Delete}
|
||||
iconAs={Icon}
|
||||
alt="Delete"
|
||||
onClick={() => {
|
||||
setCurrentTask({
|
||||
row, pageIndex: state.pageIndex, page, previousPage,
|
||||
});
|
||||
openConfirmModal();
|
||||
}}
|
||||
/>
|
||||
<IconButton src={Edit} iconAs={Icon} alt="Edit" onClick={() => handleEditEmail(row)} />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TaskAlertModal
|
||||
@@ -167,30 +187,14 @@ function BulkEmailScheduledEmailsTable({ intl }) {
|
||||
Header: 'Action',
|
||||
Cell: ({
|
||||
row, state, page, previousPage,
|
||||
}) => (
|
||||
<>
|
||||
<IconButton src={Visibility} iconAs={Icon} alt="View" onClick={() => handleViewEmail(row)} />
|
||||
<IconButton
|
||||
src={Delete}
|
||||
iconAs={Icon}
|
||||
alt="Delete"
|
||||
onClick={() => {
|
||||
setCurrentTask({
|
||||
row, pageIndex: state.pageIndex, page, previousPage,
|
||||
});
|
||||
openConfirmModal();
|
||||
}}
|
||||
/>
|
||||
<IconButton src={Edit} iconAs={Icon} alt="Edit" onClick={() => handleEditEmail(row)} />
|
||||
</>
|
||||
),
|
||||
}) => getCell(row, state, page, previousPage),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
BulkEmailScheduledEmailsTable.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './BulkEmailScheduledEmailsTable';
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './BulkEmailTool';
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { ActionRow, AlertModal, Button } from '@edx/paragon';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
function TaskAlertModal(props) {
|
||||
const TaskAlertModal = (props) => {
|
||||
const {
|
||||
isOpen, close, alertMessage, intl,
|
||||
} = props;
|
||||
@@ -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>
|
||||
@@ -50,7 +51,7 @@ function TaskAlertModal(props) {
|
||||
{alertMessage}
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
TaskAlertModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './TaskAlertModal';
|
||||
|
||||
@@ -20,7 +20,7 @@ import '@edx/tinymce-language-selector';
|
||||
import contentUiCss from 'tinymce/skins/ui/oxide/content.css';
|
||||
import contentCss from 'tinymce/skins/content/default/content.css';
|
||||
|
||||
export default function TextEditor(props) {
|
||||
const TextEditor = (props) => {
|
||||
const {
|
||||
onChange, onKeyUp, onInit, disabled, value,
|
||||
} = props;
|
||||
@@ -51,7 +51,7 @@ export default function TextEditor(props) {
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
TextEditor.defaultProps = {
|
||||
onChange: () => {},
|
||||
@@ -68,3 +68,5 @@ TextEditor.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
||||
export default TextEditor;
|
||||
|
||||
@@ -6,18 +6,16 @@ import PropTypes from 'prop-types';
|
||||
* with jest, so we need to mock it out. This is not ideal, but since the TextEditor
|
||||
* component is really just a wrapper, we're not too concerned about unit testing.
|
||||
*/
|
||||
function MockTinyMCE({ onChange }) {
|
||||
return <textarea data-testid="textEditor" onChange={onChange} />;
|
||||
}
|
||||
const MockTinyMCE = ({ onChange }) => <textarea data-testid="textEditor" onChange={onChange} />;
|
||||
|
||||
MockTinyMCE.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default function TextEditor({ onChange }) {
|
||||
return <MockTinyMCE onChange={onChange} />;
|
||||
}
|
||||
const TextEditor = ({ onChange }) => <MockTinyMCE onChange={onChange} />;
|
||||
|
||||
TextEditor.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default TextEditor;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './TextEditor';
|
||||
|
||||
@@ -5,22 +5,22 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Icon } from '@edx/paragon';
|
||||
import { ArrowBack } from '@edx/paragon/icons';
|
||||
|
||||
export default function BackToInstructor() {
|
||||
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`}
|
||||
>
|
||||
<Icon
|
||||
src={ArrowBack}
|
||||
className="mr-2"
|
||||
/>
|
||||
<FormattedMessage
|
||||
id="bulk.email.back.to.instructorDashboard"
|
||||
defaultMessage="Back to Instructor Dashboard"
|
||||
description="A link to take the user back to the instructor dashboard"
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
const BackToInstructor = () => (
|
||||
<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`}
|
||||
>
|
||||
<Icon
|
||||
src={ArrowBack}
|
||||
className="mr-2"
|
||||
/>
|
||||
<FormattedMessage
|
||||
id="bulk.email.back.to.instructorDashboard"
|
||||
defaultMessage="Back to Instructor Dashboard"
|
||||
description="A link to take the user back to the instructor dashboard"
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
|
||||
export default BackToInstructor;
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { Nav } from '@edx/paragon';
|
||||
|
||||
export default function NavigationTabs(props) {
|
||||
const NavigationTabs = (props) => {
|
||||
const { tabData } = props;
|
||||
|
||||
return (
|
||||
@@ -19,7 +19,7 @@ export default function NavigationTabs(props) {
|
||||
</Nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
NavigationTabs.propTypes = {
|
||||
tabData: PropTypes.arrayOf(PropTypes.shape({
|
||||
@@ -32,3 +32,5 @@ NavigationTabs.propTypes = {
|
||||
NavigationTabs.defaultProps = {
|
||||
tabData: [],
|
||||
};
|
||||
|
||||
export default NavigationTabs;
|
||||
|
||||
@@ -12,7 +12,7 @@ import './PageContainer.scss';
|
||||
|
||||
export const CourseMetadataContext = React.createContext();
|
||||
|
||||
export default function PageContainer(props) {
|
||||
const PageContainer = (props) => {
|
||||
const { children } = props;
|
||||
const { courseId } = useParams();
|
||||
|
||||
@@ -53,6 +53,7 @@ export default function PageContainer(props) {
|
||||
});
|
||||
}
|
||||
fetchCourseMetadata();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (courseMetadata) {
|
||||
@@ -85,8 +86,10 @@ export default function PageContainer(props) {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PageContainer.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default PageContainer;
|
||||
|
||||
@@ -52,14 +52,12 @@ export function initializeMockApp() {
|
||||
|
||||
function render(ui, options) {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
function Wrapper({ children }) {
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-filename-extension
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider>{children}</AppProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
const Wrapper = ({ children }) => (
|
||||
// eslint-disable-next-line react/jsx-filename-extension
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider>{children}</AppProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
return rtlRender(ui, { wrapper: Wrapper, ...options });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
// NOTE: These are the breakpoints used in Bootstrap v4.0.0 as seen in
|
||||
// the documentation (https://getbootstrap.com/docs/4.0/layout/overview/#responsive-breakpoints)
|
||||
@@ -29,14 +29,15 @@ const breakpoints = {
|
||||
*/
|
||||
export default function useMobileResponsive(breakpoint) {
|
||||
const [isMobileWindow, setIsMobileWindow] = useState();
|
||||
const checkForMobile = () => {
|
||||
const checkForMobile = useCallback(() => {
|
||||
setIsMobileWindow(window.matchMedia(`(max-width: ${breakpoint || breakpoints.small.maxWidth}px)`).matches);
|
||||
};
|
||||
}, [breakpoint]);
|
||||
useEffect(() => {
|
||||
checkForMobile();
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user