Compare commits
7 Commits
master
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41bd035132 | ||
|
|
eae2037d20 | ||
|
|
39c18ac4fa | ||
|
|
a4b7b5f28d | ||
|
|
42797240df | ||
|
|
ce1e7d040e | ||
|
|
3e7ea6a3e5 |
@@ -1,8 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:open-edx:p:edx-platform:r:frontend-app-communications]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
31
Makefile
31
Makefile
@@ -1,6 +1,3 @@
|
||||
export 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
|
||||
@@ -32,35 +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/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
else
|
||||
# Experimental: OEP-58 Pulls translations using atlas
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
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
|
||||
&& 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
|
||||
endif
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -12,7 +12,8 @@
|
||||
"@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.0.0",
|
||||
"@edx/frontend-platform": "5.5.2",
|
||||
"@edx/openedx-atlas": "^0.5.0",
|
||||
"@edx/paragon": "^20.44.0",
|
||||
"@edx/tinymce-language-selector": "1.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
@@ -3020,8 +3021,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-platform": {
|
||||
"version": "5.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-5.5.2.tgz",
|
||||
"integrity": "sha512-cbUvWcFL/mTc7eypBS/BnCojgWDcJCe3h3ffb3GD7F+Y4ysrFBJYf031qPcgmWNUrN30452dR7r1+sqE7uVvYA==",
|
||||
"dependencies": {
|
||||
"@cospired/i18n-iso-languages": "4.1.0",
|
||||
"@formatjs/intl-pluralrules": "4.3.3",
|
||||
@@ -3049,7 +3051,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edx/frontend-build": ">= 8.1.0 || ^12.9.0-alpha.1",
|
||||
"@edx/paragon": ">= 10.0.0 < 21.0.0",
|
||||
"@edx/paragon": ">= 10.0.0 < 22.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.9.0 || ^17.0.0",
|
||||
"react-dom": "^16.9.0 || ^17.0.0",
|
||||
@@ -3065,6 +3067,14 @@
|
||||
"@newrelic/publish-sourcemap": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/openedx-atlas": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/openedx-atlas/-/openedx-atlas-0.5.0.tgz",
|
||||
"integrity": "sha512-wgcquAVj2BVw1rS7m1LZzUg6wVku37gs/Ks14LPWefpEIQ0HcTFV4nkg8dWfGX5dP3GiEp8s1Up7XwXzlErQhQ==",
|
||||
"bin": {
|
||||
"atlas": "atlas"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/paragon": {
|
||||
"version": "20.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.46.2.tgz",
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"@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.0.0",
|
||||
"@edx/frontend-platform": "5.5.2",
|
||||
"@edx/openedx-atlas": "^0.5.0",
|
||||
"@edx/paragon": "^20.44.0",
|
||||
"@edx/tinymce-language-selector": "1.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<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>
|
||||
|
||||
@@ -33,7 +33,11 @@ export default function BulkEmailTool() {
|
||||
</h1>
|
||||
</div>
|
||||
<div className="row">
|
||||
<BulkEmailForm courseId={courseId} cohorts={courseMetadata.cohorts} />
|
||||
<BulkEmailForm
|
||||
courseId={courseId}
|
||||
cohorts={courseMetadata.cohorts}
|
||||
courseModes={courseMetadata.courseModes}
|
||||
/>
|
||||
</div>
|
||||
<div className="row py-5">
|
||||
<BulkEmailTaskManager courseId={courseId} />
|
||||
|
||||
@@ -47,7 +47,12 @@ const FORM_ACTIONS = {
|
||||
};
|
||||
|
||||
function BulkEmailForm(props) {
|
||||
const { courseId, cohorts, intl } = props;
|
||||
const {
|
||||
courseId,
|
||||
cohorts,
|
||||
courseModes,
|
||||
intl,
|
||||
} = props;
|
||||
const [{ editor }, dispatch] = useContext(BulkEmailContext);
|
||||
const [emailFormStatus, setEmailFormStatus] = useState(FORM_SUBMIT_STATES.DEFAULT);
|
||||
const [emailFormValidation, setEmailFormValidation] = useState({
|
||||
@@ -272,10 +277,14 @@ 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} />
|
||||
<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>
|
||||
{!emailFormValidation.subject && (
|
||||
<Form.Control.Feedback className="px-3" hasIcon type="invalid">
|
||||
{intl.formatMessage(messages.bulkEmailFormSubjectError)}
|
||||
@@ -384,6 +393,12 @@ 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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -14,7 +14,13 @@ const DEFAULT_GROUPS = {
|
||||
};
|
||||
|
||||
export default function BulkEmailRecipient(props) {
|
||||
const { handleCheckboxes, selectedGroups, additionalCohorts } = props;
|
||||
const {
|
||||
handleCheckboxes,
|
||||
selectedGroups,
|
||||
additionalCohorts,
|
||||
courseModes,
|
||||
} = props;
|
||||
const hasCourseModes = courseModes && courseModes.length > 1;
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
@@ -50,18 +56,24 @@ export default function BulkEmailRecipient(props) {
|
||||
description="A selectable choice from a list of potential email recipients"
|
||||
/>
|
||||
</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 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>
|
||||
))
|
||||
}
|
||||
{
|
||||
// additional cohorts
|
||||
additionalCohorts
|
||||
@@ -80,18 +92,6 @@ 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,4 +127,10 @@ 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,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
@@ -41,6 +41,11 @@ 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',
|
||||
|
||||
@@ -12,6 +12,7 @@ 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');
|
||||
|
||||
@@ -20,12 +21,17 @@ 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} />
|
||||
<BulkEmailForm
|
||||
courseId="test"
|
||||
cohorts={cohorts}
|
||||
courseModes={courseMode}
|
||||
/>
|
||||
</BulkEmailProvider>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +39,7 @@ function renderBulkEmailForm() {
|
||||
function renderBulkEmailFormContext(value) {
|
||||
return (
|
||||
<BulkEmailContext.Provider value={[value, dispatchMock]}>
|
||||
<BulkEmailForm courseId="test" />
|
||||
<BulkEmailForm courseId="test" courseMode={courseMode} />
|
||||
</BulkEmailContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -96,8 +102,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();
|
||||
|
||||
@@ -26,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(', '),
|
||||
}));
|
||||
@@ -91,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);
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function PageContainer(props) {
|
||||
}
|
||||
|
||||
const {
|
||||
org, number, title, tabs, originalUserIsStaff,
|
||||
org, number, title, tabs, originalUserIsStaff, courseModes,
|
||||
} = metadataResponse;
|
||||
const { cohorts } = cohortsResponse;
|
||||
|
||||
@@ -48,6 +48,7 @@ export default function PageContainer(props) {
|
||||
number,
|
||||
title,
|
||||
originalUserIsStaff,
|
||||
courseModes,
|
||||
tabs: [...tabs],
|
||||
cohorts: cohorts.map(({ name }) => name),
|
||||
});
|
||||
@@ -66,9 +67,11 @@ export default function PageContainer(props) {
|
||||
courseNumber={courseMetadata.number}
|
||||
courseTitle={courseMetadata.title}
|
||||
/>
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<div className="pb-3 container">
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
</CourseMetadataContext.Provider>
|
||||
|
||||
@@ -21,20 +21,18 @@ subscribe(APP_READY, () => {
|
||||
<Helmet>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
<div className="pb-3 container">
|
||||
<Routes>
|
||||
<Route
|
||||
path="/courses/:courseId/bulk_email"
|
||||
element={(
|
||||
<AuthenticatedPageRoute>
|
||||
<PageContainer>
|
||||
<BulkEmailTool />
|
||||
</PageContainer>
|
||||
</AuthenticatedPageRoute>
|
||||
)}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/courses/:courseId/bulk_email"
|
||||
element={(
|
||||
<AuthenticatedPageRoute>
|
||||
<PageContainer>
|
||||
<BulkEmailTool />
|
||||
</PageContainer>
|
||||
</AuthenticatedPageRoute>
|
||||
)}
|
||||
/>
|
||||
</Routes>
|
||||
</AppProvider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user