Skip to content

Commit 54b4ac1

Browse files
committed
Merge branch 'master' into rpenido/fal-4291/course-outline-sidebar-and-navbar
2 parents 095b731 + 68a4b04 commit 54b4ac1

33 files changed

Lines changed: 512 additions & 78 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
Override External URLs
2+
======================
3+
4+
What is getExternalLinkUrl?
5+
---------------------------
6+
7+
The `getExternalLinkUrl` function is a utility from `@edx/frontend-platform` that allows for centralized management of external URLs. It enables the override of external links through configuration, making it possible to customize external references without modifying the source code directly.
8+
9+
URLs wrapped with getExternalLinkUrl
10+
------------------------------------
11+
Use cases:
12+
13+
1. **Accessibility Page** (`src/accessibility-page/AccessibilityPage.jsx`)
14+
- `COMMUNITY_ACCESSIBILITY_LINK` - Points to community accessibility resources: https://www.edx.org/accessibility
15+
16+
2. **Course Outline** (if applicable)
17+
- Documentation links
18+
- Help resources
19+
20+
3. **Other pages** (search for `getExternalLinkUrl` usage across the codebase)
21+
- Help documentation
22+
- External tool integrations
23+
24+
Following external URLs are wrapped with `getExternalLinkUrl` in the authoring application:
25+
26+
- 'https://www.edx.org/accessibility'
27+
- 'https://docs.openedx.org/en/latest/educators/concepts/exercise_tools/about_multi_select.html'
28+
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_multi_select.html'
29+
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_dropdown.html'
30+
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/manage_numerical_input_problem.html'
31+
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_text_input.html'
32+
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html'
33+
- 'https://docs.openedx.org/en/latest/educators/references/course_development/exercise_tools/guide_problem_types.html#advanced-problem-types'
34+
- 'https://docs.openedx.org/en/latest/educators/references/course_development/parent_child_components.html'
35+
- 'https://openai.com/api-data-privacy'
36+
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/create_new_library.html'
37+
- 'https://bigbluebutton.org/privacy-policy/'
38+
- 'https://creativecommons.org/about'
39+
40+
Note: as new external URLs are added to the codebase, more URLs will be wrapped with `getExternalLinkUrl` and this list may not always be up to date.
41+
42+
How to Override External URLs
43+
-----------------------------
44+
45+
To override external URLs, you can use the frontend platform's configuration system.
46+
This object should be added to the config object defined in the env.config.[js,jsx,ts,tsx], and must be named externalLinkUrlOverrides.
47+
48+
1. **Environment Configuration**
49+
Add the URL overrides to your environment configuration:
50+
51+
.. code-block:: javascript
52+
53+
const config = {
54+
// Other config options...
55+
externalLinkUrlOverrides: {
56+
'https://www.edx.org/accessibility': 'https://your-custom-domain.com/accessibility',
57+
// Add other URL overrides here
58+
}
59+
};
60+
61+
Examples
62+
--------
63+
64+
**Original URL:** Default community accessibility link
65+
**Override:** Your institution's accessibility policy page
66+
67+
.. code-block:: javascript
68+
69+
// In your app configuration
70+
getExternalLinkUrl('https://www.edx.org/accessibility')
71+
// Returns: 'https://your-custom-domain.com/accessibility'
72+
// Instead of the default Open edX community link
73+
74+
Benefits
75+
--------
76+
77+
- **Customization**: Institutions can point to their own resources
78+
- **Maintainability**: URLs can be changed without code modifications
79+
- **Consistency**: Centralized URL management across the application
80+
- **Flexibility**: Different environments can have different external links

package-lock.json

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"@openedx/frontend-plugin-framework": "^1.7.0",
6464
"@openedx/paragon": "^23.5.0",
6565
"@redux-devtools/extension": "^3.3.0",
66-
"@reduxjs/toolkit": "2.11.1",
66+
"@reduxjs/toolkit": "2.11.2",
6767
"@tanstack/react-query": "5.90.12",
6868
"@tinymce/tinymce-react": "^6.0.0",
6969
"classnames": "2.5.1",

plugins/course-apps/live/BBBSettings.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useEffect, useState } from 'react';
2-
import { getConfig } from '@edx/frontend-platform';
2+
import { getConfig, getExternalLinkUrl } from '@edx/frontend-platform';
33
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
44
import { Form, Hyperlink } from '@openedx/paragon';
55
import PropTypes from 'prop-types';
@@ -93,7 +93,7 @@ const BbbSettings = ({
9393
<span data-testid="free-plan-message">
9494
{intl.formatMessage(messages.freePlanMessage)}
9595
<Hyperlink
96-
destination="https://bigbluebutton.org/privacy-policy/"
96+
destination={getExternalLinkUrl('https://bigbluebutton.org/privacy-policy/')}
9797
target="_blank"
9898
rel="noopener noreferrer"
9999
showLaunchIcon

plugins/course-apps/xpert_unit_summary/settings-modal/SettingsModal.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useIntl } from '@edx/frontend-platform/i18n';
2+
import { getExternalLinkUrl } from '@edx/frontend-platform';
23
import {
34
ActionRow,
45
Alert,
@@ -276,7 +277,7 @@ const SettingsModal = ({
276277
<div className="py-1">
277278
<Hyperlink
278279
className="text-primary-500"
279-
destination="https://openai.com/api-data-privacy"
280+
destination={getExternalLinkUrl('https://openai.com/api-data-privacy')}
280281
target="_blank"
281282
rel="noreferrer noopener"
282283
>

src/accessibility-page/AccessibilityPage.jsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import { useIntl } from '@edx/frontend-platform/i18n';
3+
import { getExternalLinkUrl } from '@edx/frontend-platform';
34
import { Helmet } from 'react-helmet';
45
import { Container } from '@openedx/paragon';
56
import { StudioFooterSlot } from '@edx/frontend-component-footer';
@@ -23,9 +24,12 @@ const AccessibilityPage = () => {
2324
</title>
2425
</Helmet>
2526
<Header isHiddenMainMenu />
26-
<Container size="xl" classNamae="px-4">
27+
<Container size="xl" className="px-4">
2728
<AccessibilityBody
28-
{...{ email: ACCESSIBILITY_EMAIL, communityAccessibilityLink: COMMUNITY_ACCESSIBILITY_LINK }}
29+
{...{
30+
email: ACCESSIBILITY_EMAIL,
31+
communityAccessibilityLink: getExternalLinkUrl(COMMUNITY_ACCESSIBILITY_LINK),
32+
}}
2933
/>
3034
<AccessibilityForm accessibilityEmail={ACCESSIBILITY_EMAIL} />
3135
</Container>

src/course-unit/CourseUnit.test.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2191,7 +2191,7 @@ describe('<CourseUnit />', () => {
21912191

21922192
const currentSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
21932193
const currentSubSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
2194-
const helpLinkUrl = 'https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_components.html#components-that-contain-other-components';
2194+
const helpLinkUrl = 'https://docs.openedx.org/en/latest/educators/references/course_development/parent_child_components.html';
21952195

21962196
await waitFor(() => {
21972197
const unitHeaderTitle = screen.getByTestId('unit-header-title');

src/course-unit/sidebar/SplitTestSidebarInfo.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import { Card, Hyperlink, Stack } from '@openedx/paragon';
33
import { useIntl } from '@edx/frontend-platform/i18n';
4+
import { getExternalLinkUrl } from '@edx/frontend-platform';
45

56
import messages from './messages';
67

@@ -44,7 +45,7 @@ const SplitTestSidebarInfo = () => {
4445
<hr className="course-split-test-sidebar-devider my-4" />
4546
<Hyperlink
4647
showLaunchIcon={false}
47-
destination="https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_components.html#components-that-contain-other-components"
48+
destination={getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/references/course_development/parent_child_components.html')}
4849
className="btn btn-outline-primary btn-sm"
4950
target="_blank"
5051
>

src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import * as hooks from './hooks';
1919
import { ProblemTypeKeys } from '../../../../../data/constants/problem';
2020
import ExpandableTextArea from '../../../../../sharedComponents/ExpandableTextArea';
2121
import { answerRangeFormatRegex } from '../../../data/OLXParser';
22+
import { useValidateInputBlock } from '../../../data/apiHooks';
2223

2324
const AnswerOption = ({
2425
answer,
@@ -32,7 +33,6 @@ const AnswerOption = ({
3233
const isLibrary = useSelector(selectors.app.isLibrary);
3334
const learningContextId = useSelector(selectors.app.learningContextId);
3435
const blockId = useSelector(selectors.app.blockId);
35-
3636
const removeAnswer = hooks.removeAnswer({ answer, dispatch });
3737
const setAnswer = hooks.setAnswer({ answer, hasSingleAnswer, dispatch });
3838
const setAnswerTitle = hooks.setAnswerTitle({
@@ -44,6 +44,7 @@ const AnswerOption = ({
4444
const setSelectedFeedback = hooks.setSelectedFeedback({ answer, hasSingleAnswer, dispatch });
4545
const setUnselectedFeedback = hooks.setUnselectedFeedback({ answer, hasSingleAnswer, dispatch });
4646
const { isFeedbackVisible, toggleFeedback } = hooks.useFeedback(answer);
47+
const { data = { isValid: true }, mutate } = useValidateInputBlock();
4748

4849
const staticRootUrl = isLibrary
4950
? `${getConfig().STUDIO_BASE_URL}/library_assets/blocks/${blockId}/`
@@ -69,17 +70,31 @@ const AnswerOption = ({
6970
/>
7071
);
7172
}
73+
7274
if (problemType !== ProblemTypeKeys.NUMERIC || !answer.isAnswerRange) {
7375
return (
74-
<Form.Control
75-
as="textarea"
76-
className="answer-option-textarea text-gray-500 small"
77-
autoResize
78-
rows={1}
79-
value={answer.title}
80-
onChange={setAnswerTitle}
81-
placeholder={intl.formatMessage(messages.answerTextboxPlaceholder)}
82-
/>
76+
<Form.Group isInvalid={!data?.isValid ?? true}>
77+
<Form.Control
78+
as="textarea"
79+
className="answer-option-textarea text-gray-500 small"
80+
autoResize
81+
rows={1}
82+
value={answer.title}
83+
onChange={(e) => {
84+
setAnswerTitle(e);
85+
if (problemType === ProblemTypeKeys.NUMERIC) {
86+
mutate(e.target.value);
87+
}
88+
}}
89+
placeholder={intl.formatMessage(messages.answerTextboxPlaceholder)}
90+
91+
/>
92+
{(!data?.isValid ?? true) && (
93+
<Form.Control.Feedback type="invalid">
94+
<FormattedMessage {...messages.answerNumericErrorText} />
95+
</Form.Control.Feedback>
96+
)}
97+
</Form.Group>
8398
);
8499
}
85100
// Return Answer Range View

src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { render, screen, initializeMocks } from '@src/testUtils';
33
import { selectors } from '@src/editors/data/redux';
44
import AnswerOption from './AnswerOption';
55
import * as hooks from './hooks';
6+
import * as reactQueryHooks from '../../../data/apiHooks';
67

78
const { problem } = selectors;
89

@@ -101,4 +102,16 @@ describe('AnswerOption', () => {
101102
expect(screen.getByText(answerRange.title)).toBeInTheDocument();
102103
expect(screen.getByRole('textbox')).toBeInTheDocument();
103104
});
105+
106+
test('shows numeric error feedback when data.isValid is false', () => {
107+
// Mock useValidateInputBlock to simulate invalid state
108+
// @ts-ignore-next-line
109+
jest.spyOn(reactQueryHooks, 'useValidateInputBlock').mockReturnValue({ data: { isValid: false } });
110+
jest.spyOn(problem, 'problemType').mockReturnValue('numericalresponse');
111+
const myProps = { ...props, answer: { ...answerWithOnlyFeedback, isAnswerRange: false } };
112+
render(<AnswerOption {...myProps} />);
113+
expect(
114+
screen.getByText('Error: This input type only supports numeric answers. Did you mean to make a Text input or Math expression input problem?'),
115+
).toBeInTheDocument();
116+
});
104117
});

0 commit comments

Comments
 (0)