diff --git a/.eslintignore b/.eslintignore index 65588a7767..2222a3388c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ coverage/* dist/ node_modules/ +src/i18n/ src/segment.js diff --git a/.eslintrc.js b/.eslintrc.js index 0e96647f4e..cf41a2a1c3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,7 +5,26 @@ const config = getBaseConfig('eslint'); /* Custom config manipulations */ config.rules = { ...config.rules, - 'default-param-last': 'off', + '@typescript-eslint/default-param-last': 'off', + 'react/require-default-props': 'off', + 'import/no-named-as-default': 0, }; +config.ignorePatterns = ["*.json", ".eslintrc.js", "*.config.js", "jsdom-with-global.js"]; + +config.overrides = [ + { + files: ['*.test.js', '*.test.jsx'], + parser: "@typescript-eslint/parser", + parserOptions: { + project: [ + "./tsconfig.json", + "./functions/tsconfig.json", + ] + } + }, +]; + + + module.exports = config; diff --git a/.gitignore b/.gitignore index 1fb5b30eaf..2f81fc6ff0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ dist/ # edx .env.private +src/i18n/ +temp/ diff --git a/Makefile b/Makefile index 13d2ef4569..98b0cc7e28 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,11 @@ +transifex_utils = ./node_modules/.bin/transifex-utils.js +intl_imports = ./node_modules/frontend-platform-shim/node_modules/.bin/intl-imports.js + +i18n = ./src/i18n +transifex_input = $(i18n)/transifex_input.json +# This directory must match .babelrc . +transifex_temp = ./temp/babel-plugin-react-intl + shell: ## run a shell on the cookie-cutter container docker exec -it /bin/bash @@ -30,3 +38,26 @@ restart-detached: validate-no-uncommitted-package-lock-changes: git diff --name-only --exit-code package-lock.json + +requirements: + npm install + +i18n.extract: + # Pulling display strings from .jsx files into .json files... + rm -rf $(transifex_temp) + npm run-script i18n_extract + +i18n.concat: + # Gathering JSON messages into one file... + $(transifex_utils) $(transifex_temp) $(transifex_input) + +extract_translations: | requirements i18n.extract i18n.concat + +pull_translations: + rm -rf src/i18n/messages + mkdir src/i18n/messages + cd src/i18n/messages \ + && atlas pull \ + translations/paragon/src/i18n/messages:paragon \ + translations/frontend-app-admin-portal/src/i18n/messages:frontend-app-admin-portal + $(intl_imports) paragon frontend-app-admin-portal diff --git a/__mocks__/react-instantsearch-dom.jsx b/__mocks__/react-instantsearch-dom.jsx index 56f4d71b7c..f62f6554ca 100644 --- a/__mocks__/react-instantsearch-dom.jsx +++ b/__mocks__/react-instantsearch-dom.jsx @@ -7,7 +7,7 @@ const MockReactInstantSearch = jest.genMockFromModule( 'react-instantsearch-dom', ); -// eslint-disable-next-line camelcase +// eslint-disable-next-line @typescript-eslint/naming-convention const advertised_course_run = { start: '2020-09-09T04:00:00Z', key: 'course-v1:edX+Bee101+3T2020', diff --git a/docs/how_tos/i18n.rst b/docs/how_tos/i18n.rst new file mode 100644 index 0000000000..8fbb1896f7 --- /dev/null +++ b/docs/how_tos/i18n.rst @@ -0,0 +1,5 @@ +#################### +React App i18n HOWTO +#################### + +This document has moved to the frontend-platform repo: https://github.com/openedx/frontend-platform/blob/master/docs/how_tos/i18n.rst diff --git a/package-lock.json b/package-lock.json index 45b79d93b6..b3c64050bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "dash-embedded-component": "file:packages/dash-embedded-component-2.0.2.tgz", "file-saver": "1.3.8", "font-awesome": "4.7.0", + "frontend-platform-shim": "file:packages/frontend-platform-shim", "history": "4.10.1", "html-react-parser": "3.0.7", "jest-environment-jsdom": "26.6.1", @@ -10926,6 +10927,10 @@ "node": ">= 0.6" } }, + "node_modules/frontend-platform-shim": { + "resolved": "packages/frontend-platform-shim", + "link": true + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -23178,6 +23183,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "packages/frontend-platform-shim": { + "version": "1.0.0", + "dependencies": { + "@edx/frontend-platform": "4.5.1" + }, + "peerDependencies": { + "@edx/frontend-platform": "<4.1.0" + } } } } diff --git a/package.json b/package.json index 1a4e3f9a25..3df219f765 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,9 @@ "scripts": { "build": "fedx-scripts webpack", "build:with-theme": "THEME=npm:@edx/brand-edx.org@latest npm run install-theme && fedx-scripts webpack", - "lint": "fedx-scripts eslint --ext .js --ext .jsx .", - "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .", + "check-types": "tsc --noemit", + "lint": "fedx-scripts eslint --ext .js --ext .jsx .; npm run check-types", + "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .tsx --ext .ts .", "precommit": "npm run lint", "prepublishOnly": "npm run build", "install-theme": "npm install \"@edx/brand@${THEME}\" --no-save", @@ -45,6 +46,7 @@ "dash-embedded-component": "file:packages/dash-embedded-component-2.0.2.tgz", "file-saver": "1.3.8", "font-awesome": "4.7.0", + "frontend-platform-shim": "file:packages/frontend-platform-shim", "history": "4.10.1", "html-react-parser": "3.0.7", "jest-environment-jsdom": "26.6.1", @@ -109,4 +111,4 @@ "resize-observer-polyfill": "1.5.1", "ts-jest": "^26.5.0" } -} +} \ No newline at end of file diff --git a/packages/frontend-platform-shim/package.json b/packages/frontend-platform-shim/package.json new file mode 100644 index 0000000000..47dbfc8dab --- /dev/null +++ b/packages/frontend-platform-shim/package.json @@ -0,0 +1,11 @@ +{ + "name": "frontend-platform-shim", + "version": "1.0.0", + "description": "Shim package to install the `intl-imports.js` script from frontend-platform@4.1+ until the `frontend-app-admin-portal` upgrades from frontend-platform@2.x.x . This package should be removed once the `frontend-app-admin-portal` is upgraded therefore the `peerDependencies` pin to ensure the TODO is coded into the dependency tree.", + "dependencies": { + "@edx/frontend-platform": "4.5.1" + }, + "peerDependencies": { + "@edx/frontend-platform": "<4.1.0" + } +} diff --git a/src/components/Admin/Admin.test.jsx b/src/components/Admin/Admin.test.jsx index 30ca049b38..4362ae6805 100644 --- a/src/components/Admin/Admin.test.jsx +++ b/src/components/Admin/Admin.test.jsx @@ -6,6 +6,7 @@ import { MemoryRouter, Link } from 'react-router-dom'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import EnterpriseDataApiService from '../../data/services/EnterpriseDataApiService'; import Admin from './index'; @@ -40,26 +41,28 @@ const store = mockStore({ const AdminWrapper = props => ( - {}} - fetchDashboardAnalytics={() => {}} - fetchPortalConfiguration={() => {}} - fetchCsv={() => {}} - searchEnrollmentsList={() => {}} - tableData={[ - { - course_title: 'Bears 101', - course_start: Date.now(), - }, - ]} - match={{ - params: {}, - url: '/', - }} - {...props} - /> + + {}} + fetchDashboardAnalytics={() => {}} + fetchPortalConfiguration={() => {}} + fetchCsv={() => {}} + searchEnrollmentsList={() => {}} + tableData={[ + { + course_title: 'Bears 101', + course_start: Date.now(), + }, + ]} + match={{ + params: {}, + url: '/', + }} + {...props} + /> + ); diff --git a/src/components/Admin/AdminCards.jsx b/src/components/Admin/AdminCards.jsx index 5d8a92eb31..d1afe4ac95 100644 --- a/src/components/Admin/AdminCards.jsx +++ b/src/components/Admin/AdminCards.jsx @@ -1,46 +1,76 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + import NumberCard from '../NumberCard'; class AdminCards extends React.Component { constructor(props) { super(props); + const { intl } = this.props; this.cards = { numberOfUsers: { ref: React.createRef(), - description: 'total number of learners registered', + description: intl.formatMessage({ + id: 'adminPortal.cards.registeredLearners', + defaultMessage: 'total number of learners registered', + }), iconClassName: 'fa fa-users', actions: [{ - label: 'Which learners are registered but not yet enrolled in any courses?', + label: intl.formatMessage({ + id: 'adminPortal.cards.registeredUnenrolledLearners', + defaultMessage: 'Which learners are registered but not yet enrolled in any courses?', + }), slug: 'registered-unenrolled-learners', }], }, enrolledLearners: { ref: React.createRef(), - description: 'learners enrolled in at least one course', + description: intl.formatMessage({ + id: 'adminPortal.cards.enrolledOneCourse', + defaultMessage: 'learners enrolled in at least one course', + }), iconClassName: 'fa fa-check', actions: [{ - label: 'How many courses are learners enrolled in?', + label: intl.formatMessage({ + id: 'adminPortal.cards.enrolledLearners', + defaultMessage: 'How many courses are learners enrolled in?', + }), slug: 'enrolled-learners', }, { - label: 'Who is no longer enrolled in a current course?', + label: intl.formatMessage({ + id: 'adminPortal.cards.enrolledLearnersInactiveCourses', + defaultMessage: 'Who is no longer enrolled in a current course?', + }), slug: 'enrolled-learners-inactive-courses', }], }, activeLearners: { ref: React.createRef(), - description: 'active learners in the past week', + description: intl.formatMessage({ + id: 'adminPortal.cards.activeLearnersPastWeek', + defaultMessage: 'active learners in the past week', + }), iconClassName: 'fa fa-eye', actions: [{ - label: 'Who are my top active learners?', + label: intl.formatMessage({ + id: 'adminPortal.cards.learnersActiveWeek', + defaultMessage: 'Who are my top active learners?', + }), slug: 'learners-active-week', }, { - label: 'Who has not been active for over a week?', + label: intl.formatMessage({ + id: 'adminPortal.cards.learnersInactiveWeek', + defaultMessage: 'Who has not been active for over a week?', + }), slug: 'learners-inactive-week', }, { - label: 'Who has not been active for over a month?', + label: intl.formatMessage({ + id: 'adminPortal.cards.learnersInactiveMonth', + defaultMessage: 'Who has not been active for over a month?', + }), slug: 'learners-inactive-month', }], }, @@ -49,10 +79,16 @@ class AdminCards extends React.Component { description: 'course completions', iconClassName: 'fa fa-trophy', actions: [{ - label: 'How many courses have been completed by learners?', + label: intl.formatMessage({ + id: 'adminPortal.cards.completedLearners', + defaultMessage: 'How many courses have been completed by learners?', + }), slug: 'completed-learners', }, { - label: 'Who completed a course in the past week?', + label: intl.formatMessage({ + id: 'adminPortal.cards.completedLearnersWeek', + defaultMessage: 'Who completed a course in the past week?', + }), slug: 'completed-learners-week', }], }, @@ -110,6 +146,8 @@ AdminCards.propTypes = { numberOfUsers: PropTypes.number.isRequired, courseCompletions: PropTypes.number.isRequired, enrolledLearners: PropTypes.number.isRequired, + // injected + intl: intlShape.isRequired, }; -export default AdminCards; +export default injectIntl(AdminCards); diff --git a/src/components/Admin/SubscriptionDetailPage.jsx b/src/components/Admin/SubscriptionDetailPage.jsx index dde917dae8..56552b08ec 100644 --- a/src/components/Admin/SubscriptionDetailPage.jsx +++ b/src/components/Admin/SubscriptionDetailPage.jsx @@ -10,7 +10,7 @@ import { useSubscriptionFromParams } from '../subscriptions/data/contextHooks'; import SubscriptionDetailsSkeleton from '../subscriptions/SubscriptionDetailsSkeleton'; import { LPR_SUBSCRIPTION_PAGE_SIZE } from '../subscriptions/data/constants'; -// eslint-disable-next-line no-unused-vars +// eslint-disable-next-line @typescript-eslint/no-unused-vars export const SubscriptionDetailPage = ({ enterpriseSlug, match }) => { const [subscription, loadingSubscription] = useSubscriptionFromParams({ match }); diff --git a/src/components/CodeAssignmentModal/CodeAssignmentModal.test.jsx b/src/components/CodeAssignmentModal/CodeAssignmentModal.test.jsx index ff5b45bce0..a62e71dc20 100644 --- a/src/components/CodeAssignmentModal/CodeAssignmentModal.test.jsx +++ b/src/components/CodeAssignmentModal/CodeAssignmentModal.test.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Provider } from 'react-redux'; - +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { screen, render } from '@testing-library/react'; // import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom/extend-expect'; @@ -11,13 +11,12 @@ import thunk from 'redux-thunk'; import { MemoryRouter } from 'react-router-dom'; import remindEmailTemplate from './emailTemplate'; import CodeAssignmentModal, { BaseCodeAssignmentModal } from '.'; -import { ASSIGNMENT_MODAL_FIELDS, NOTIFY_LEARNERS_CHECKBOX_TEST_ID } from './constants'; +import { getAssignmentModalFields, NOTIFY_LEARNERS_CHECKBOX_TEST_ID } from './constants'; import { displayCode, displaySelectedCodes } from '../CodeModal/codeModalHelpers'; import { EMAIL_TEMPLATE_SOURCE_NEW_EMAIL, } from '../../data/constants/emailTemplate'; -import { EMAIL_FORM_NAME } from '../EmailTemplateForm'; jest.mock('redux-form', () => ({ ...jest.requireActual('redux-form'), @@ -122,14 +121,20 @@ const initialState = { }, }; +const mockIntl = { + formatMessage: message => message.defaultMessage, +}; + /* eslint-disable react/prop-types */ const CodeAssignmentModalWrapper = (props) => ( - + + + ); @@ -141,16 +146,19 @@ describe('CodeAssignmentModal component', () => { expect(screen.getByText(initialProps.title)).toBeInTheDocument(); }); it('displays an error', () => { - // eslint-disable-next-line global-require, no-unused-vars + // eslint-disable-next-line @typescript-eslint/no-unused-vars, global-require const { Field } = require('redux-form'); const error = 'Errors ahoy!'; const props = { ...initialProps, error: [error], submitFailed: true }; render( - + + + , ); @@ -167,11 +175,13 @@ describe('CodeAssignmentModal component', () => { }); it('renders an email template form', () => { render(); - expect(screen.getByText(EMAIL_FORM_NAME)).toBeInTheDocument(); + expect(screen.getByText('Email Template')).toBeInTheDocument(); }); it('renders a auto-reminder checkbox', () => { + const formatMessageMock = message => message.defaultMessage; + const assignmentModalFields = getAssignmentModalFields(formatMessageMock); render(); - expect(screen.getByText(ASSIGNMENT_MODAL_FIELDS['enable-nudge-emails'].label)).toBeInTheDocument(); + expect(screen.getByText(assignmentModalFields['enable-nudge-emails'].label)).toBeInTheDocument(); }); it('renders notify learners toggle checkbox', () => { render(); diff --git a/src/components/CodeAssignmentModal/constants.jsx b/src/components/CodeAssignmentModal/constants.jsx index 8d2e36977f..995adefa65 100644 --- a/src/components/CodeAssignmentModal/constants.jsx +++ b/src/components/CodeAssignmentModal/constants.jsx @@ -1,27 +1,32 @@ import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; import { MODAL_TYPES } from '../EmailTemplateForm/constants'; -import { EMAIL_TEMPLATE_FIELDS } from '../EmailTemplateForm'; +import { getTemplateEmailFields } from '../EmailTemplateForm'; import CheckboxWithTooltip from '../ReduxFormCheckbox/CheckboxWithTooltip'; +import messages from './messages'; + export const ASSIGNMENT_ERROR_TITLES = { [MODAL_TYPES.assign]: 'Unable to assign codes', [MODAL_TYPES.save]: 'Unable to save template', }; export const EMAIL_TEMPLATE_NUDGE_EMAIL_ID = 'enable-nudge-emails'; -export const ASSIGNMENT_MODAL_FIELDS = { - ...EMAIL_TEMPLATE_FIELDS, - [EMAIL_TEMPLATE_NUDGE_EMAIL_ID]: { - name: EMAIL_TEMPLATE_NUDGE_EMAIL_ID, - id: EMAIL_TEMPLATE_NUDGE_EMAIL_ID, - component: CheckboxWithTooltip, - className: 'auto-reminder-wrapper', - icon: faInfoCircle, - altText: 'More information', - tooltipText: 'edX will remind learners to redeem their code 3, 10, and 19 days after you assign it.', - label: 'Automate reminders', - defaultChecked: true, - }, +export const getAssignmentModalFields = formatMessage => { + const emailTemplateFields = getTemplateEmailFields(formatMessage); + return { + ...emailTemplateFields, + [EMAIL_TEMPLATE_NUDGE_EMAIL_ID]: { + name: EMAIL_TEMPLATE_NUDGE_EMAIL_ID, + id: EMAIL_TEMPLATE_NUDGE_EMAIL_ID, + component: CheckboxWithTooltip, + className: 'auto-reminder-wrapper', + icon: faInfoCircle, + altText: formatMessage(messages.modalAltText), + tooltipText: formatMessage(messages.modalTooltipText), + label: formatMessage(messages.modalFieldLabel), + defaultChecked: true, + }, + }; }; export const NOTIFY_LEARNERS_CHECKBOX_TEST_ID = 'notify-learners-checkbox'; diff --git a/src/components/CodeAssignmentModal/index.jsx b/src/components/CodeAssignmentModal/index.jsx index e594793bf5..e492fef0b2 100644 --- a/src/components/CodeAssignmentModal/index.jsx +++ b/src/components/CodeAssignmentModal/index.jsx @@ -4,6 +4,8 @@ import { reduxForm, SubmissionError } from 'redux-form'; import { Button, Icon, Modal, Form, } from '@edx/paragon'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + import isEmail from 'validator/lib/isEmail'; import BulkAssignFields from './BulkAssignFields'; @@ -27,9 +29,9 @@ import EmailTemplateForm from '../EmailTemplateForm'; import { EMAIL_TEMPLATE_NUDGE_EMAIL_ID, ASSIGNMENT_ERROR_TITLES, - ASSIGNMENT_MODAL_FIELDS, NOTIFY_LEARNERS_CHECKBOX_TEST_ID, SUBMIT_BUTTON_TEST_ID, + getAssignmentModalFields, } from './constants'; import { getErrors } from './validation'; @@ -339,6 +341,7 @@ export class BaseCodeAssignmentModal extends React.Component { isBulkAssign, submitFailed, error, + intl: { formatMessage }, } = this.props; const { mode, notify } = this.state; @@ -376,7 +379,7 @@ export class BaseCodeAssignmentModal extends React.Component { { notify && ( )} @@ -478,8 +481,11 @@ BaseCodeAssignmentModal.propTypes = { unassignedCodes: PropTypes.number, remainingUses: PropTypes.number, }), + + // injected + intl: intlShape.isRequired, }; export default reduxForm({ form: 'code-assignment-modal-form', -})(BaseCodeAssignmentModal); +})(injectIntl(BaseCodeAssignmentModal)); diff --git a/src/components/CodeAssignmentModal/messages.js b/src/components/CodeAssignmentModal/messages.js new file mode 100644 index 0000000000..455bf9667a --- /dev/null +++ b/src/components/CodeAssignmentModal/messages.js @@ -0,0 +1,18 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + modalAltText: { + id: 'adminPortal.assignmentModal.altText', + defaultMessage: 'More information', + }, + modalTooltipText: { + id: 'adminPortal.assignmentModal.tooltipText', + defaultMessage: 'edX will remind learners to redeem their code 3, 10, and 19 days after you assign it.', + }, + modalFieldLabel: { + id: 'adminPortal.assignmentModal.modalFieldLabel', + defaultMessage: 'Automate reminders', + }, +}); + +export default messages; diff --git a/src/components/CodeReminderModal/CodeReminderModal.test.jsx b/src/components/CodeReminderModal/CodeReminderModal.test.jsx index 8ae8c93505..75e250f063 100644 --- a/src/components/CodeReminderModal/CodeReminderModal.test.jsx +++ b/src/components/CodeReminderModal/CodeReminderModal.test.jsx @@ -4,6 +4,7 @@ import { Provider } from 'react-redux'; import { screen, render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { MemoryRouter } from 'react-router-dom'; @@ -14,7 +15,6 @@ import { displayCode, displayEmail, displaySelectedCodes } from '../CodeModal/co import { EMAIL_TEMPLATE_SOURCE_NEW_EMAIL, } from '../../data/constants/emailTemplate'; -import { EMAIL_FORM_NAME } from '../EmailTemplateForm'; jest.mock('redux-form', () => ({ ...jest.requireActual('redux-form'), @@ -114,14 +114,23 @@ const initialState = { }, }; +const mockIntl = { + formatMessage(message) { + return message.defaultMessage; + }, +}; + /* eslint-disable react/prop-types */ const CodeReminderModalWrapper = (props) => ( - + + + ); @@ -149,6 +158,6 @@ describe('CodeReminderModal component', () => { }); it('renders an email template form', () => { render(); - expect(screen.getByText(EMAIL_FORM_NAME)).toBeInTheDocument(); + expect(screen.getByText('Email Template')).toBeInTheDocument(); }); }); diff --git a/src/components/CodeReminderModal/index.jsx b/src/components/CodeReminderModal/index.jsx index eb3c77dcd9..fdd22cb562 100644 --- a/src/components/CodeReminderModal/index.jsx +++ b/src/components/CodeReminderModal/index.jsx @@ -2,6 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { reduxForm, SubmissionError } from 'redux-form'; import { Button, Icon, Modal } from '@edx/paragon'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + import SaveTemplateButton from '../../containers/SaveTemplateButton'; import { EMAIL_TEMPLATE_SUBJECT_KEY } from '../../data/constants/emailTemplate'; @@ -10,17 +12,10 @@ import ModalError from '../CodeModal/ModalError'; import { configuration, features } from '../../config'; import './CodeReminderModal.scss'; import CodeDetails from './CodeDetails'; -import EmailTemplateForm, { EMAIL_TEMPLATE_FIELDS } from '../EmailTemplateForm'; +import EmailTemplateForm, { getTemplateEmailFields } from '../EmailTemplateForm'; import { EMAIL_TEMPLATE_FILES_ID, MODAL_TYPES } from '../EmailTemplateForm/constants'; import { appendUserCodeDetails } from '../CodeModal'; -const REMINDER_EMAIL_TEMPLATE_FIELDS = { - ...EMAIL_TEMPLATE_FIELDS, - 'email-template-body': { - ...EMAIL_TEMPLATE_FIELDS['email-template-body'], - disabled: true, - }, -}; const REMIND_MODE = MODAL_TYPES.remind; const ERROR_MESSAGE_TITLES = { @@ -169,13 +164,21 @@ export class BaseCodeReminderModal extends React.Component { const { data, isBulkRemind, + intl: { formatMessage }, submitFailed, error, } = this.props; const { mode } = this.state; const numberOfSelectedCodes = this.getNumberOfSelectedCodes(); - + const emailTemplateFields = getTemplateEmailFields(formatMessage); + const reminderEmailTemplateFields = { + ...emailTemplateFields, + 'email-template-body': { + ...emailTemplateFields['email-template-body'], + disabled: true, + }, + }; return ( <> {submitFailed && ( @@ -193,7 +196,7 @@ export class BaseCodeReminderModal extends React.Component { /> ); @@ -282,8 +285,10 @@ BaseCodeReminderModal.propTypes = { code: PropTypes.string, email: PropTypes.string, }), + // injected + intl: intlShape.isRequired, }; export default reduxForm({ form: 'code-reminder-modal-form', -})(BaseCodeReminderModal); +})(injectIntl(BaseCodeReminderModal)); diff --git a/src/components/CodeRevokeModal/CodeRevokeModal.test.jsx b/src/components/CodeRevokeModal/CodeRevokeModal.test.jsx index c0bae14188..37be085c64 100644 --- a/src/components/CodeRevokeModal/CodeRevokeModal.test.jsx +++ b/src/components/CodeRevokeModal/CodeRevokeModal.test.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { Provider } from 'react-redux'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { screen, render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; @@ -16,7 +17,6 @@ import { displayCode, displaySelectedCodes } from '../CodeModal/codeModalHelpers import { EMAIL_TEMPLATE_SOURCE_NEW_EMAIL, } from '../../data/constants/emailTemplate'; -import { EMAIL_FORM_NAME } from '../EmailTemplateForm'; const sampleCodeData = { code: 'test-code-1', @@ -120,10 +120,12 @@ const initialState = { const CodeRevokeModalWrapper = (props) => ( - + + + ); @@ -144,7 +146,7 @@ describe('CodeRevokeModal component', () => { }); it('renders an email template form', () => { render(); - expect(screen.getByText(EMAIL_FORM_NAME)).toBeInTheDocument(); + expect(screen.getByText('Email Template')).toBeInTheDocument(); }); it('renders a auto-reminder checkbox', () => { render(); diff --git a/src/components/CodeSearchResults/CodeSearchResults.test.jsx b/src/components/CodeSearchResults/CodeSearchResults.test.jsx index be04bd8b12..a5b8e19c3e 100644 --- a/src/components/CodeSearchResults/CodeSearchResults.test.jsx +++ b/src/components/CodeSearchResults/CodeSearchResults.test.jsx @@ -1,10 +1,13 @@ import React from 'react'; +import PropTypes from 'prop-types'; + import { Provider } from 'react-redux'; import renderer from 'react-test-renderer'; import { MemoryRouter } from 'react-router-dom'; import { mount } from 'enzyme'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import CodeSearchResults from './index'; @@ -62,6 +65,24 @@ const initialStore = { }, }; +const CodeSearchResultsWrapper = props => ( + + + + + + + +); + +CodeSearchResultsWrapper.propTypes = { + store: PropTypes.shape({}), +}; + +CodeSearchResultsWrapper.defaultProps = { + store: null, +}; + describe('', () => { beforeAll(() => { const mockPromiseResolve = () => Promise.resolve({ data: {} }); @@ -72,19 +93,14 @@ describe('', () => { describe('basic rendering', () => { it('should render nothing visible when isOpen prop is false', () => { - const store = getMockStore({ ...initialStore }); const tree = renderer .create(( - - - - - + )) .toJSON(); expect(tree).toMatchSnapshot(); @@ -103,15 +119,12 @@ describe('', () => { }); const tree = renderer .create(( - - - - - + )) .toJSON(); expect(tree).toMatchSnapshot(); @@ -162,15 +175,12 @@ describe('', () => { }); const tree = renderer .create(( - - - - - + )) .toJSON(); expect(tree).toMatchSnapshot(); @@ -202,15 +212,12 @@ describe('', () => { }); const tree = renderer .create(( - - - - - + )) .toJSON(); expect(tree).toMatchSnapshot(); @@ -233,15 +240,12 @@ describe('', () => { }); const tree = renderer .create(( - - - - - + )) .toJSON(); expect(tree).toMatchSnapshot(); @@ -260,15 +264,12 @@ describe('', () => { }); const tree = renderer .create(( - - - - - + )) .toJSON(); expect(tree).toMatchSnapshot(); @@ -303,15 +304,12 @@ describe('', () => { }, }); const wrapper = mount(( - - - - - + )); const mockPromiseResolve = () => Promise.resolve({ data: {} }); EcommerceApiService.fetchEmailTemplate.mockImplementation(mockPromiseResolve); @@ -358,15 +356,12 @@ describe('', () => { }, }); const wrapper = mount(( - - - - - + )); const mockPromiseResolve = () => Promise.resolve({ data: {} }); EcommerceApiService.fetchEmailTemplate.mockImplementation(mockPromiseResolve); @@ -403,15 +398,12 @@ describe('', () => { }, }); const wrapper = mount(( - - - - - + )); expect(wrapper.find('CodeSearchResults').state('isCodeRevokeSuccessful')).toBeFalsy(); wrapper.find('RevokeButton').simulate('click'); @@ -439,15 +431,12 @@ describe('', () => { }, }); const wrapper = mount(( - - - - - + )); wrapper.find('.close-search-results-btn').first().simulate('click'); diff --git a/src/components/ContentHighlights/HighlightStepper/HighlightStepperSelectContentSearch.jsx b/src/components/ContentHighlights/HighlightStepper/HighlightStepperSelectContentSearch.jsx index e29eb4dc12..9d59ca3fcf 100644 --- a/src/components/ContentHighlights/HighlightStepper/HighlightStepperSelectContentSearch.jsx +++ b/src/components/ContentHighlights/HighlightStepper/HighlightStepperSelectContentSearch.jsx @@ -25,44 +25,6 @@ const selectColumn = { disableSortBy: true, }; -const HighlightStepperSelectContent = ({ enterpriseId }) => { - const { setCurrentSelectedRowIds } = useContentHighlightsContext(); - const currentSelectedRowIds = useContextSelector( - ContentHighlightsContext, - v => v[0].stepperModal.currentSelectedRowIds, - ); - const searchClient = useContextSelector( - ContentHighlightsContext, - v => v[0].searchClient, - ); - // TODO: replace testEnterpriseId with enterpriseId before push, - // uncomment out import and replace with testEnterpriseId to test - const searchFilters = `enterprise_customer_uuids:${ENABLE_TESTING(enterpriseId)}`; - - return ( - - - - - - - - ); -}; - -HighlightStepperSelectContent.propTypes = { - enterpriseId: PropTypes.string.isRequired, -}; - const PriceTableCell = ({ row }) => { const contentPrice = row.original.firstEnrollablePaidSeatPrice; if (!contentPrice) { @@ -176,4 +138,42 @@ BaseHighlightStepperSelectContentDataTable.defaultProps = { const HighlightStepperSelectContentDataTable = connectStateResults(BaseHighlightStepperSelectContentDataTable); +const HighlightStepperSelectContent = ({ enterpriseId }) => { + const { setCurrentSelectedRowIds } = useContentHighlightsContext(); + const currentSelectedRowIds = useContextSelector( + ContentHighlightsContext, + v => v[0].stepperModal.currentSelectedRowIds, + ); + const searchClient = useContextSelector( + ContentHighlightsContext, + v => v[0].searchClient, + ); + // TODO: replace testEnterpriseId with enterpriseId before push, + // uncomment out import and replace with testEnterpriseId to test + const searchFilters = `enterprise_customer_uuids:${ENABLE_TESTING(enterpriseId)}`; + + return ( + + + + + + + + ); +}; + +HighlightStepperSelectContent.propTypes = { + enterpriseId: PropTypes.string.isRequired, +}; + export default HighlightStepperSelectContent; diff --git a/src/components/ContentHighlights/data/constants.js b/src/components/ContentHighlights/data/constants.js index a0a4ebab64..978b549410 100644 --- a/src/components/ContentHighlights/data/constants.js +++ b/src/components/ContentHighlights/data/constants.js @@ -155,7 +155,7 @@ export const LEARNER_PORTAL_CATALOG_VISIBILITY = { export const DEFAULT_ERROR_MESSAGE = { EMPTY_HIGHLIGHT_SET: 'There is no highlighted content for this highlight collection.', // eslint-disable-next-line quotes - EMPTY_SELECTEDROWIDS: `You don't have any highlighted content selected. Go back to the previous step to select content.`, + EMPTY_SELECTEDROWIDS: 'You don\'t have any highlighted content selected. Go back to the previous step to select content.', EXCEEDS_HIGHLIGHT_TITLE_LENGTH: `Titles may only be ${MAX_HIGHLIGHT_TITLE_LENGTH} characters or less`, }; diff --git a/src/components/ContentHighlights/tests/ContentHighlightSet.test.jsx b/src/components/ContentHighlights/tests/ContentHighlightSet.test.jsx index c1aecb2691..776d8fbfc8 100644 --- a/src/components/ContentHighlights/tests/ContentHighlightSet.test.jsx +++ b/src/components/ContentHighlights/tests/ContentHighlightSet.test.jsx @@ -44,8 +44,7 @@ jest.mock('react-router-dom', () => ({ useParams: jest.fn(), })); -/* eslint-disable react/prop-types */ -// eslint-disable-next-line no-unused-vars +// eslint-disable-next-line @typescript-eslint/no-unused-vars const ContentHighlightSetWrapper = ( enterpriseAppContextValue = initialEnterpriseAppContextValue, { children }, diff --git a/src/components/Coupon/Coupon.test.jsx b/src/components/Coupon/Coupon.test.jsx index 37473ed9d1..3d61077d8a 100644 --- a/src/components/Coupon/Coupon.test.jsx +++ b/src/components/Coupon/Coupon.test.jsx @@ -5,6 +5,7 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { shallow, mount } from 'enzyme'; import { MemoryRouter } from 'react-router-dom'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { MULTI_USE } from '../../data/constants/coupons'; @@ -47,10 +48,12 @@ const initialCouponData = { const CouponWrapper = props => ( - + + + ); diff --git a/src/components/CouponDetails/index.test.jsx b/src/components/CouponDetails/index.test.jsx index b2edf62f15..d034ce266f 100644 --- a/src/components/CouponDetails/index.test.jsx +++ b/src/components/CouponDetails/index.test.jsx @@ -6,6 +6,8 @@ import { MemoryRouter } from 'react-router-dom'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import userEvent from '@testing-library/user-event'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + import { renderWithRouter } from '../test/testUtils'; import CouponDetails from './index'; import { COUPON_FILTERS, DEFAULT_TABLE_COLUMNS } from './constants'; @@ -135,9 +137,11 @@ const defaultProps = { const CouponDetailsWrapper = props => ( - + + + ); diff --git a/src/components/DownloadCsvButton/index.jsx b/src/components/DownloadCsvButton/index.jsx index dd3cb10772..702c37e7e8 100644 --- a/src/components/DownloadCsvButton/index.jsx +++ b/src/components/DownloadCsvButton/index.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button, Icon } from '@edx/paragon'; import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; export const CSV_CLICK_SEGMENT_EVENT_NAME = 'edx.ui.enterprise.admin_portal.download_csv.clicked'; @@ -20,6 +21,7 @@ class DownloadCsvButton extends React.Component { enterpriseId, id, } = this.props; + const downloadButtonIconClasses = csvLoading ? ['fa-spinner', 'fa-spin'] : ['fa-download']; return ( ); @@ -46,7 +48,7 @@ DownloadCsvButton.defaultProps = { csvLoading: false, fetchMethod: () => {}, disabled: false, - buttonLabel: 'Download full report (CSV)', + buttonLabel: '', }; DownloadCsvButton.propTypes = { diff --git a/src/components/EmailTemplateForm/EmailTemplateForm.test.jsx b/src/components/EmailTemplateForm/EmailTemplateForm.test.jsx index 3d4cc5a141..3b8190dd7e 100644 --- a/src/components/EmailTemplateForm/EmailTemplateForm.test.jsx +++ b/src/components/EmailTemplateForm/EmailTemplateForm.test.jsx @@ -1,13 +1,14 @@ import React from 'react'; import { Provider } from 'react-redux'; import { reduxForm } from 'redux-form'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import { screen, render } from '@testing-library/react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import '@testing-library/jest-dom/extend-expect'; import { MemoryRouter } from 'react-router'; -import EmailTemplateForm, { EMAIL_FORM_NAME, EMAIL_TEMPLATE_FIELDS } from '.'; +import EmailTemplateForm, { getTemplateEmailFields } from '.'; import { MODAL_TYPES } from './constants'; import { TEMLATE_SOURCE_FIELDS_TEST_ID } from '../TemplateSourceFields'; import RenderField from '../RenderField'; @@ -16,6 +17,8 @@ const mockStore = configureMockStore([thunk]); const ConnectedEmailTemplateForm = reduxForm({ form: 'test' })(EmailTemplateForm); +const mockFormatMessage = message => message.defaultMessage; + const initialState = { emailTemplate: { emailTemplateSource: 'foo', @@ -26,7 +29,9 @@ const initialState = { const EmailTemplateFormWrapper = (props) => ( - + + + ); @@ -34,11 +39,12 @@ const EmailTemplateFormWrapper = (props) => ( describe('EmailTemplateForm', () => { it('renders a form', () => { render(); - expect(screen.getByText(EMAIL_FORM_NAME)).toBeInTheDocument(); + expect(screen.getByText('Email Template')).toBeInTheDocument(); }); it('renders default fields', () => { + const emailTemplateFields = getTemplateEmailFields(mockFormatMessage); render(); - Object.values(EMAIL_TEMPLATE_FIELDS).forEach((field) => { + Object.values(emailTemplateFields).forEach((field) => { expect(screen.getByText(field.label)).toBeInTheDocument(); }); }); @@ -56,9 +62,10 @@ describe('EmailTemplateForm', () => { type: 'text', }, }; + const emailTemplateFields = getTemplateEmailFields(mockFormatMessage); render(); expect(screen.getByText(fields.foo.label)).toBeInTheDocument(); - Object.values(EMAIL_TEMPLATE_FIELDS).forEach((field) => { + Object.values(emailTemplateFields).forEach((field) => { expect(screen.queryByText(field.label)).not.toBeInTheDocument(); }); }); diff --git a/src/components/EmailTemplateForm/index.jsx b/src/components/EmailTemplateForm/index.jsx index b1c61335a9..5efe025768 100644 --- a/src/components/EmailTemplateForm/index.jsx +++ b/src/components/EmailTemplateForm/index.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Field } from 'redux-form'; +import { useIntl } from '@edx/frontend-platform/i18n'; import TemplateSourceFields from '../../containers/TemplateSourceFields'; import TextAreaAutoSize from '../TextAreaAutoSize'; import RenderField from '../RenderField'; @@ -16,12 +17,13 @@ import { } from './constants'; import { EMAIL_TEMPLATE_FIELD_MAX_LIMIT, OFFER_ASSIGNMENT_EMAIL_SUBJECT_LIMIT } from '../../data/constants/emailTemplate'; -export const EMAIL_FORM_NAME = 'Email Template'; -export const EMAIL_TEMPLATE_FIELDS = { +import messages from './messages'; + +export const getTemplateEmailFields = (formatMessage) => ({ [EMAIL_TEMPLATE_SUBJECT_ID]: { name: EMAIL_TEMPLATE_SUBJECT_ID, component: RenderField, - label: 'Customize email subject', + label: formatMessage(messages.emailCustomizeSubject), type: 'text', limit: OFFER_ASSIGNMENT_EMAIL_SUBJECT_LIMIT, 'data-hj-suppress': true, @@ -29,21 +31,21 @@ export const EMAIL_TEMPLATE_FIELDS = { [EMAIL_TEMPLATE_GREETING_ID]: { name: EMAIL_TEMPLATE_GREETING_ID, component: TextAreaAutoSize, - label: 'Customize greeting', + label: formatMessage(messages.emailCustomizeGreeting), limit: EMAIL_TEMPLATE_FIELD_MAX_LIMIT, 'data-hj-suppress': true, }, [EMAIL_TEMPLATE_BODY_ID]: { name: EMAIL_TEMPLATE_BODY_ID, component: TextAreaAutoSize, - label: 'Body', + label: formatMessage(messages.emailBody), disabled: true, 'data-hj-suppress': true, }, [EMAIL_TEMPLATE_CLOSING_ID]: { name: EMAIL_TEMPLATE_CLOSING_ID, component: TextAreaAutoSize, - label: 'Customize closing', + label: formatMessage(messages.emailCustomizeClosing), limit: EMAIL_TEMPLATE_FIELD_MAX_LIMIT, 'data-hj-suppress': true, }, @@ -52,29 +54,36 @@ export const EMAIL_TEMPLATE_FIELDS = { name: EMAIL_TEMPLATE_FILES_ID, component: MultipleFileInputField, type: 'file', - label: 'add files', + label: formatMessage(messages.emailAddFiles), value: [], - description: "Max files size shouldn't exceed 250kb.", + description: formatMessage(messages.emailMaxFileSizeMessage), }, }), -}; +}); const EmailTemplateForm = ({ children, emailTemplateType, fields, currentEmail, disabled, -}) => ( -
e.preventDefault()}> -
-

{EMAIL_FORM_NAME}

- - {Object.values(fields).map(fieldProps => )} - {children} -
-
-); +}) => { + const { formatMessage } = useIntl(); + const fieldsWithDefault = fields || getTemplateEmailFields(formatMessage); + + return ( +
e.preventDefault()}> +
+

{formatMessage(messages.emailFormName)}

+ + {Object.values(fieldsWithDefault).map(fieldProps => ( + + ))} + {children} +
+
+ ); +}; EmailTemplateForm.defaultProps = { children: null, - fields: EMAIL_TEMPLATE_FIELDS, + fields: null, currentEmail: '', disabled: false, }; diff --git a/src/components/EmailTemplateForm/messages.js b/src/components/EmailTemplateForm/messages.js new file mode 100644 index 0000000000..5aafd7b712 --- /dev/null +++ b/src/components/EmailTemplateForm/messages.js @@ -0,0 +1,34 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + emailFormName: { + id: 'adminPortal.emailTemplateForm.formName', + defaultMessage: 'Email Template', + }, + emailCustomizeSubject: { + id: 'adminPortal.emailTemplateForm.customizeSubject', + defaultMessage: 'Customize email subject', + }, + emailCustomizeGreeting: { + id: 'adminPortal.emailTemplateForm.customizeGreeting', + defaultMessage: 'Customize greeting', + }, + emailBody: { + id: 'adminPortal.emailTemplateForm.body', + defaultMessage: 'Body', + }, + emailCustomizeClosing: { + id: 'adminPortal.emailTemplateForm.customizeClosing', + defaultMessage: 'Customize closing', + }, + emailAddFiles: { + id: 'adminPortal.emailTemplateForm.addFiles', + defaultMessage: 'add files', + }, + emailMaxFileSizeMessage: { + id: 'adminPortal.emailTemplateForm.maxFileSizeMessage', + defaultMessage: "Max files size shouldn't exceed 250kb.", + }, +}); + +export default messages; diff --git a/src/components/EnrollmentsTable/EnrollmentsTable.test.jsx b/src/components/EnrollmentsTable/EnrollmentsTable.test.jsx index 9c3e01b977..c627a2737c 100644 --- a/src/components/EnrollmentsTable/EnrollmentsTable.test.jsx +++ b/src/components/EnrollmentsTable/EnrollmentsTable.test.jsx @@ -4,6 +4,7 @@ import renderer from 'react-test-renderer'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { Provider } from 'react-redux'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import EnrollmentsTable from './index'; // import EnterpriseDataApiService from '../../data/services/EnterpriseDataApiService'; @@ -32,9 +33,11 @@ const store = mockStore({ const EnrollmentsWrapper = props => ( - + + + ); diff --git a/src/components/EnrollmentsTable/index.jsx b/src/components/EnrollmentsTable/index.jsx index e53a63b5be..c1f0fb29b7 100644 --- a/src/components/EnrollmentsTable/index.jsx +++ b/src/components/EnrollmentsTable/index.jsx @@ -1,53 +1,84 @@ import React from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; + import TableContainer from '../../containers/TableContainer'; import { formatTimestamp, formatPassedTimestamp, formatPercentage } from '../../utils'; import EnterpriseDataApiService from '../../data/services/EnterpriseDataApiService'; const EnrollmentsTable = () => { + const intl = useIntl(); + const enrollmentTableColumns = [ { - label: 'Email', + label: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.user_email', + defaultMessage: 'Email', + }), key: 'user_email', columnSortable: true, }, { - label: 'Course Title', + label: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.courseTitle', + defaultMessage: 'Course Title', + }), key: 'course_title', columnSortable: true, }, { - label: 'Course Price', + label: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.courseListPrice', + defaultMessage: 'Course Price', + }), key: 'course_list_price', columnSortable: true, }, { - label: 'Start Date', + label: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.courseStartDate', + defaultMessage: 'Start Date', + }), key: 'course_start_date', columnSortable: true, }, { - label: 'End Date', + label: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.courseEndDate', + defaultMessage: 'End Date', + }), key: 'course_end_date', columnSortable: true, }, { - label: 'Passed Date', + label: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.passedDate', + defaultMessage: 'Passed Date', + }), key: 'passed_date', columnSortable: true, }, { - label: 'Current Grade', + label: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.currentGrade', + defaultMessage: 'Current Grade', + }), key: 'current_grade', columnSortable: true, }, { - label: 'Progress Status', + label: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.progressStatus', + defaultMessage: 'Progress Status', + }), key: 'progress_status', columnSortable: true, }, { - label: 'Last Activity Date', + label: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.lastActivityDate', + defaultMessage: 'Last Activity Date', + }), key: 'last_activity_date', columnSortable: true, }, diff --git a/src/components/EnterpriseList/EnterpriseList.test.jsx b/src/components/EnterpriseList/EnterpriseList.test.jsx index 416435d60a..47b04098c0 100644 --- a/src/components/EnterpriseList/EnterpriseList.test.jsx +++ b/src/components/EnterpriseList/EnterpriseList.test.jsx @@ -4,6 +4,7 @@ import { MemoryRouter, Redirect } from 'react-router-dom'; import { mount } from 'enzyme'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import EnterpriseList, { TITLE } from './index'; import mockEnterpriseList from './EnterpriseList.mocks'; @@ -38,14 +39,16 @@ const store = mockStore({ const EnterpriseListWrapper = ({ initialEntries, ...rest }) => ( - {}} - clearPortalConfiguration={() => {}} - {...rest} - /> + + {}} + clearPortalConfiguration={() => {}} + {...rest} + /> + ); diff --git a/src/components/ErrorPage/ErrorPage.test.jsx b/src/components/ErrorPage/ErrorPage.test.jsx index 7f5fd40fd4..d5df5e94ab 100644 --- a/src/components/ErrorPage/ErrorPage.test.jsx +++ b/src/components/ErrorPage/ErrorPage.test.jsx @@ -1,16 +1,23 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { MemoryRouter } from 'react-router-dom'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; import ErrorPage from './index'; +const ErrorPageWrapper = (props) => ( + + + + + +); + describe('', () => { it('renders correctly', () => { const tree = renderer .create(( - - - + )) .toJSON(); expect(tree).toMatchSnapshot(); @@ -19,9 +26,7 @@ describe('', () => { it('renders correctly for 404 errors', () => { const tree = renderer .create(( - - - + )) .toJSON(); expect(tree).toMatchSnapshot(); @@ -30,9 +35,7 @@ describe('', () => { it('renders correctly for 403 errors', () => { const tree = renderer .create(( - - - + )) .toJSON(); expect(tree).toMatchSnapshot(); diff --git a/src/components/ErrorPage/index.jsx b/src/components/ErrorPage/index.jsx index c319aef2a3..3f614fea33 100644 --- a/src/components/ErrorPage/index.jsx +++ b/src/components/ErrorPage/index.jsx @@ -4,6 +4,8 @@ import Helmet from 'react-helmet'; import { Alert } from '@edx/paragon'; import { Cancel as ErrorIcon } from '@edx/paragon/icons'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; + import NotFoundPage from '../NotFoundPage'; import ForbiddenPage from '../ForbiddenPage'; @@ -28,7 +30,7 @@ function renderErrorComponent(status, message) { variant="danger" icon={ErrorIcon} > - Error + {errorMessage} diff --git a/src/components/Footer/index.jsx b/src/components/Footer/index.jsx index 824efe33ce..192f8dbfd5 100644 --- a/src/components/Footer/index.jsx +++ b/src/components/Footer/index.jsx @@ -1,9 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + import { configuration } from '../../config'; import Img from '../Img'; +import messages from './messages'; import './Footer.scss'; class Footer extends React.Component { @@ -39,6 +42,8 @@ class Footer extends React.Component { render() { const { enterpriseLogoNotFound } = this.state; const { enterpriseLogo } = this.props; + const { formatMessage } = this.props.intl; + return (