diff --git a/src/accessibility-page/AccessibilityPage.jsx b/src/accessibility-page/AccessibilityPage.jsx index c37cdf6ab..e7a0a2770 100644 --- a/src/accessibility-page/AccessibilityPage.jsx +++ b/src/accessibility-page/AccessibilityPage.jsx @@ -9,30 +9,30 @@ import messages from './messages'; import AccessibilityBody from './AccessibilityBody'; import AccessibilityForm from './AccessibilityForm'; +import { COMMUNITY_ACCESSIBILITY_LINK, ACCESSIBILITY_EMAIL } from './constants'; + const AccessibilityPage = ({ // injected intl, -}) => { - const communityAccessibilityLink = 'https://www.edx.org/accessibility'; - const email = 'accessibility@edx.org'; - return ( - <> - - - {intl.formatMessage(messages.pageTitle, { - siteName: process.env.SITE_NAME, - })} - - -
- - - - - - - ); -}; +}) => ( + <> + + + {intl.formatMessage(messages.pageTitle, { + siteName: process.env.SITE_NAME, + })} + + +
+ + + + + + +); AccessibilityPage.propTypes = { // injected diff --git a/src/accessibility-page/constants.ts b/src/accessibility-page/constants.ts new file mode 100644 index 000000000..84d1a444c --- /dev/null +++ b/src/accessibility-page/constants.ts @@ -0,0 +1,2 @@ +export const COMMUNITY_ACCESSIBILITY_LINK = 'https://www.edx.org/accessibility'; +export const ACCESSIBILITY_EMAIL = 'accessibility@edx.org'; diff --git a/src/editors/containers/EditorContainer/components/TitleHeader/EditableHeader.jsx b/src/editors/containers/EditorContainer/components/TitleHeader/EditableHeader.jsx index ba212f162..0483bffe5 100644 --- a/src/editors/containers/EditorContainer/components/TitleHeader/EditableHeader.jsx +++ b/src/editors/containers/EditorContainer/components/TitleHeader/EditableHeader.jsx @@ -2,8 +2,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Form } from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; import EditConfirmationButtons from './EditConfirmationButtons'; +import messages from './messages'; + const EditableHeader = ({ handleChange, updateTitle, @@ -11,20 +14,23 @@ const EditableHeader = ({ inputRef, localTitle, cancelEdit, -}) => ( - updateTitle(e)}> - } - onChange={handleChange} - onKeyDown={handleKeyDown} - placeholder="Title" - ref={inputRef} - value={localTitle} - /> - -); +}) => { + const intl = useIntl(); + return ( + updateTitle(e)}> + } + onChange={handleChange} + onKeyDown={handleKeyDown} + placeholder={intl.formatMessage(messages.editTitlePlaceholder)} + ref={inputRef} + value={localTitle} + /> + + ); +}; EditableHeader.defaultProps = { inputRef: null, }; diff --git a/src/editors/containers/EditorContainer/components/TitleHeader/messages.js b/src/editors/containers/EditorContainer/components/TitleHeader/messages.js index 54d92ac16..0ffbffa7e 100644 --- a/src/editors/containers/EditorContainer/components/TitleHeader/messages.js +++ b/src/editors/containers/EditorContainer/components/TitleHeader/messages.js @@ -17,6 +17,11 @@ const messages = defineMessages({ defaultMessage: 'Edit Title', description: 'Screen reader label title for icon button to edit the xblock title', }, + editTitlePlaceholder: { + id: 'authoring.texteditor.header.editTitleLabelPlaceholder', + defaultMessage: 'Title', + description: 'Screen reader label title for icon button to edit the xblock title', + }, cancelTitleEdit: { id: 'authoring.texteditor.header.cancelTitleEdit', defaultMessage: 'Cancel', diff --git a/src/editors/containers/VideoEditor/components/SelectVideoModal.jsx b/src/editors/containers/VideoEditor/components/SelectVideoModal.jsx index 07167d65c..bd7a7507f 100644 --- a/src/editors/containers/VideoEditor/components/SelectVideoModal.jsx +++ b/src/editors/containers/VideoEditor/components/SelectVideoModal.jsx @@ -3,36 +3,33 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { Button } from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { thunkActions } from '../../../data/redux'; import BaseModal from '../../../sharedComponents/BaseModal'; -// This 'module' self-import hack enables mocking during tests. -// See src/editors/decisions/0005-internal-editor-testability-decisions.md. The whole approach to how hooks are tested -// should be re-thought and cleaned up to avoid this pattern. -// eslint-disable-next-line import/no-self-import -import * as module from './SelectVideoModal'; +import messages from './messages'; -export const hooks = { - videoList: ({ fetchVideos }) => { +export const useVideoList = ({ fetchVideos }) => { // eslint-disable-next-line react-hooks/rules-of-hooks - const [videos, setVideos] = React.useState(null); - // eslint-disable-next-line react-hooks/rules-of-hooks - React.useEffect(() => { - fetchVideos({ onSuccess: setVideos }); - }, []); - return videos; - }, - onSelectClick: ({ setSelection, videos }) => () => setSelection(videos[0]), + const [videos, setVideos] = React.useState(null); + // eslint-disable-next-line react-hooks/rules-of-hooks + React.useEffect(() => { + fetchVideos({ onSuccess: setVideos }); + }, []); + return videos; }; +export const useOnSelectClick = ({ setSelection, videos }) => () => setSelection(videos[0]); + export const SelectVideoModal = ({ fetchVideos, isOpen, close, setSelection, }) => { - const videos = module.hooks.videoList({ fetchVideos }); - const onSelectClick = module.hooks.onSelectClick({ + const intl = useIntl(); + const videos = useVideoList({ fetchVideos }); + const onSelectClick = useOnSelectClick({ setSelection, videos, }); @@ -41,13 +38,13 @@ export const SelectVideoModal = ({ Next} > {/* Content selection */} {videos && (videos.map( img => ( -
+
{img.externalUrl}
), ))} @@ -62,7 +59,7 @@ SelectVideoModal.propTypes = { fetchVideos: PropTypes.func.isRequired, }; -export const mapStateToProps = () => ({}); +export const mapStateToProps = null; export const mapDispatchToProps = { fetchVideos: thunkActions.app.fetchVideos, }; diff --git a/src/editors/containers/VideoEditor/components/SelectVideoModal.test.jsx b/src/editors/containers/VideoEditor/components/SelectVideoModal.test.jsx new file mode 100644 index 000000000..6901b878a --- /dev/null +++ b/src/editors/containers/VideoEditor/components/SelectVideoModal.test.jsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { + render, screen, fireEvent, waitFor, +} from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; +import { SelectVideoModal, useVideoList, useOnSelectClick } from './SelectVideoModal'; +import messages from './messages'; + +describe('SelectVideoModal', () => { + const mockStore = configureStore([]); + const mockFetchVideos = jest.fn(); + const mockSetSelection = jest.fn(); + const mockClose = jest.fn(); + let store; + + beforeEach(() => { + store = mockStore({}); + jest.clearAllMocks(); + }); + + it('renders the modal with the correct title', () => { + render( + + + + + , + ); + + expect(screen.getByText(messages.selectVideoModalTitle.defaultMessage)).toBeInTheDocument(); + }); + + it('calls close when the modal is closed', () => { + render( + + + + + , + ); + + fireEvent.click(screen.getByRole('button', { name: /close/i })); + expect(mockClose).toHaveBeenCalled(); + }); + + it('renders a div for each video in the videos array', () => { + const videos = [ + { externalUrl: 'video1.mp4' }, + { externalUrl: 'video2.mp4' }, + ]; + const fetchVideos = jest.fn(({ onSuccess }) => onSuccess(videos)); + + render( + + + + + , + ); + + // Assert that a div is rendered for each video + videos.forEach((video) => { + expect(screen.getByText(video.externalUrl)).toBeInTheDocument(); + }); + }); +}); + +describe('SelectVideoModal hooks', () => { + describe('useVideoList', () => { + it('fetches videos and sets them in state', async () => { + // eslint-disable-next-line react/prop-types + const TestComponent = ({ fetchVideos }) => { + const videos = useVideoList({ fetchVideos }); + return ( +
+ {videos ? videos.map((v) => v.externalUrl).join(', ') : 'Loading'} +
+ ); + }; + + const videos = [ + { externalUrl: 'video1.mp4' }, + { externalUrl: 'video2.mp4' }, + ]; + const fetchVideos = jest.fn(({ onSuccess }) => { + onSuccess(videos); + }); + + render(); + + await waitFor(() => expect(screen.getByTestId('videos').textContent).toBe(videos.map((v) => v.externalUrl).join(', '))); + + expect(fetchVideos).toHaveBeenCalled(); + }); + }); + + describe('useOnSelectClick', () => { + it('calls setSelection with the first video', () => { + const mockSetSelection = jest.fn(); + const videos = [ + { externalUrl: 'video1.mp4' }, + { externalUrl: 'video2.mp4' }, + ]; + + const onSelectClick = useOnSelectClick({ + setSelection: mockSetSelection, + videos, + }); + + onSelectClick(); + expect(mockSetSelection).toHaveBeenCalledWith(videos[0]); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/components/messages.ts b/src/editors/containers/VideoEditor/components/messages.ts new file mode 100644 index 000000000..9777ecbf8 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/messages.ts @@ -0,0 +1,11 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + selectVideoModalTitle: { + id: 'authoring.videoEditor.selectVideoModal.title', + defaultMessage: 'Add a video', + description: 'Title for the select video modal.', + }, +}); + +export default messages; diff --git a/src/generic/configure-modal/AdvancedTab.jsx b/src/generic/configure-modal/AdvancedTab.jsx index 73b7a4801..ba639099e 100644 --- a/src/generic/configure-modal/AdvancedTab.jsx +++ b/src/generic/configure-modal/AdvancedTab.jsx @@ -5,7 +5,7 @@ import { Alert, Form, Hyperlink } from '@openedx/paragon'; import { Warning as WarningIcon, } from '@openedx/paragon/icons'; -import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; import PrereqSettings from './PrereqSettings'; @@ -32,6 +32,8 @@ const AdvancedTab = ({ } = values; let examTypeValue = 'none'; + const intl = useIntl(); + if (isTimeLimited && isProctoredExam) { if (isOnboardingExam) { examTypeValue = 'onboardingExam'; @@ -183,7 +185,7 @@ const AdvancedTab = ({ diff --git a/src/generic/configure-modal/messages.js b/src/generic/configure-modal/messages.js index 2d1574b76..7a141ad96 100644 --- a/src/generic/configure-modal/messages.js +++ b/src/generic/configure-modal/messages.js @@ -239,6 +239,11 @@ const messages = defineMessages({ id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-allotted', defaultMessage: 'Time allotted (HH:MM):', }, + timeLimitPlaceholder: { + id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-limit-placeholder', + defaultMessage: 'HH:MM', + description: 'The placeholder for the time limit input field, two digits for hours and two digits for minutes colons in between', + }, timeLimitDescription: { id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-limit-description', defaultMessage: 'Select a time allotment for the exam. If it is over 24 hours, type in the amount of time. You can grant individual learners extra time to complete the exam through the Instructor Dashboard.', diff --git a/src/studio-home/tabs-section/courses-tab/courses-filters/index.jsx b/src/studio-home/tabs-section/courses-tab/courses-filters/index.jsx index acd7a8df1..fdea9411e 100644 --- a/src/studio-home/tabs-section/courses-tab/courses-filters/index.jsx +++ b/src/studio-home/tabs-section/courses-tab/courses-filters/index.jsx @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { SearchField } from '@openedx/paragon'; import { debounce } from 'lodash'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { getStudioHomeCoursesParams } from '../../../data/selectors'; import { updateStudioHomeCoursesCustomParams } from '../../../data/slice'; @@ -11,6 +12,7 @@ import { LoadingSpinner } from '../../../../generic/Loading'; import CoursesTypesFilterMenu from './courses-types-filter-menu'; import CoursesOrderFilterMenu from './courses-order-filter-menu'; import './index.scss'; +import messages from './messages'; /* regex to check if a string has only whitespace example " " @@ -33,6 +35,8 @@ const CoursesFilters = ({ } = studioHomeCoursesParams; const [inputSearchValue, setInputSearchValue] = useState(''); + const intl = useIntl(); + const getFilterTypeData = (baseFilters) => ({ archivedCourses: { ...baseFilters, archivedOnly: true, activeOnly: undefined }, activeCourses: { ...baseFilters, activeOnly: true, archivedOnly: undefined }, @@ -107,7 +111,7 @@ const CoursesFilters = ({ value={cleanFilters ? '' : inputSearchValue} className="mr-4" data-testid="input-filter-courses-search" - placeholder="Search" + placeholder={intl.formatMessage(messages.coursesSearchPlaceholder)} /> {isLoading && ( diff --git a/src/studio-home/tabs-section/courses-tab/courses-filters/messages.js b/src/studio-home/tabs-section/courses-tab/courses-filters/messages.js new file mode 100644 index 000000000..d477348e7 --- /dev/null +++ b/src/studio-home/tabs-section/courses-tab/courses-filters/messages.js @@ -0,0 +1,11 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + + coursesSearchPlaceholder: { + id: 'course-authoring.studio-home.courses.search-placeholder', + defaultMessage: 'Search', + }, +}); + +export default messages; diff --git a/src/taxonomy/import-tags/ImportTagsWizard.jsx b/src/taxonomy/import-tags/ImportTagsWizard.jsx index 275b966ed..31e7b1b70 100644 --- a/src/taxonomy/import-tags/ImportTagsWizard.jsx +++ b/src/taxonomy/import-tags/ImportTagsWizard.jsx @@ -40,7 +40,7 @@ const ExportStep = ({ taxonomy }) => { const intl = useIntl(); return ( - +

{intl.formatMessage(messages.importWizardStepExportBody, { br: linebreak })}

@@ -97,7 +97,7 @@ const UploadStep = ({ }; return ( - +

{ reimport @@ -190,7 +190,7 @@ const PopulateStep = ({ }; return ( - + { intl.formatMessage(messages.importWizardStepPopulateTaxonomyName) } @@ -222,7 +222,7 @@ const PlanStep = ({ importPlan }) => { const intl = useIntl(); return ( - + {intl.formatMessage(messages.importWizardStepPlanBody, { br: linebreak, changeCount: importPlan?.length })}

    @@ -249,7 +249,7 @@ const ConfirmStep = ({ importPlan }) => { const intl = useIntl(); return ( - + {intl.formatMessage( messages.importWizardStepConfirmBody, diff --git a/src/taxonomy/import-tags/messages.ts b/src/taxonomy/import-tags/messages.ts index 1561c8a69..cef6e89ca 100644 --- a/src/taxonomy/import-tags/messages.ts +++ b/src/taxonomy/import-tags/messages.ts @@ -154,6 +154,26 @@ const messages = defineMessages({ id: 'course-authoring.import-tags.error-alert.title', defaultMessage: 'Import error', }, + importWizardStepperExportStepTitle: { + id: 'course-authoring.import-tags.wizard.stepper.export-step.title', + defaultMessage: 'Export', + }, + importWizardStepperUploadStepTitle: { + id: 'course-authoring.import-tags.wizard.stepper.upload-step.title', + defaultMessage: 'Upload', + }, + importWizardStepperPopulateStepTitle: { + id: 'course-authoring.import-tags.wizard.stepper.populate-step.title', + defaultMessage: 'Populate', + }, + importWizardStepperPlanStepTitle: { + id: 'course-authoring.import-tags.wizard.stepper.plan-step.title', + defaultMessage: 'Plan', + }, + importWizardStepperConfirmStepTitle: { + id: 'course-authoring.import-tags.wizard.stepper.confirm-step.title', + defaultMessage: 'Confirm', + }, }); export default messages;