Add external links override support (#2730)

* feat: add external links override support

* fix: add note, fix 404 url and fix unrelated typo

* test: fix
This commit is contained in:
Diana Villalvazo
2025-12-19 13:06:03 -06:00
committed by GitHub
parent 9072bb66b7
commit c2592a7e6e
11 changed files with 112 additions and 21 deletions

View File

@@ -0,0 +1,80 @@
Override External URLs
======================
What is getExternalLinkUrl?
---------------------------
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.
URLs wrapped with getExternalLinkUrl
------------------------------------
Use cases:
1. **Accessibility Page** (`src/accessibility-page/AccessibilityPage.jsx`)
- `COMMUNITY_ACCESSIBILITY_LINK` - Points to community accessibility resources: https://www.edx.org/accessibility
2. **Course Outline** (if applicable)
- Documentation links
- Help resources
3. **Other pages** (search for `getExternalLinkUrl` usage across the codebase)
- Help documentation
- External tool integrations
Following external URLs are wrapped with `getExternalLinkUrl` in the authoring application:
- 'https://www.edx.org/accessibility'
- 'https://docs.openedx.org/en/latest/educators/concepts/exercise_tools/about_multi_select.html'
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_multi_select.html'
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_dropdown.html'
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/manage_numerical_input_problem.html'
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_text_input.html'
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html'
- 'https://docs.openedx.org/en/latest/educators/references/course_development/exercise_tools/guide_problem_types.html#advanced-problem-types'
- 'https://docs.openedx.org/en/latest/educators/references/course_development/parent_child_components.html'
- 'https://openai.com/api-data-privacy'
- 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/create_new_library.html'
- 'https://bigbluebutton.org/privacy-policy/'
- 'https://creativecommons.org/about'
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.
How to Override External URLs
-----------------------------
To override external URLs, you can use the frontend platform's configuration system.
This object should be added to the config object defined in the env.config.[js,jsx,ts,tsx], and must be named externalLinkUrlOverrides.
1. **Environment Configuration**
Add the URL overrides to your environment configuration:
.. code-block:: javascript
const config = {
// Other config options...
externalLinkUrlOverrides: {
'https://www.edx.org/accessibility': 'https://your-custom-domain.com/accessibility',
// Add other URL overrides here
}
};
Examples
--------
**Original URL:** Default community accessibility link
**Override:** Your institution's accessibility policy page
.. code-block:: javascript
// In your app configuration
getExternalLinkUrl('https://www.edx.org/accessibility')
// Returns: 'https://your-custom-domain.com/accessibility'
// Instead of the default Open edX community link
Benefits
--------
- **Customization**: Institutions can point to their own resources
- **Maintainability**: URLs can be changed without code modifications
- **Consistency**: Centralized URL management across the application
- **Flexibility**: Different environments can have different external links

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { getConfig, getExternalLinkUrl } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Form, Hyperlink } from '@openedx/paragon';
import PropTypes from 'prop-types';
@@ -93,7 +93,7 @@ const BbbSettings = ({
<span data-testid="free-plan-message">
{intl.formatMessage(messages.freePlanMessage)}
<Hyperlink
destination="https://bigbluebutton.org/privacy-policy/"
destination={getExternalLinkUrl('https://bigbluebutton.org/privacy-policy/')}
target="_blank"
rel="noopener noreferrer"
showLaunchIcon

View File

@@ -1,4 +1,5 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { getExternalLinkUrl } from '@edx/frontend-platform';
import {
ActionRow,
Alert,
@@ -276,7 +277,7 @@ const SettingsModal = ({
<div className="py-1">
<Hyperlink
className="text-primary-500"
destination="https://openai.com/api-data-privacy"
destination={getExternalLinkUrl('https://openai.com/api-data-privacy')}
target="_blank"
rel="noreferrer noopener"
>

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getExternalLinkUrl } from '@edx/frontend-platform';
import { Helmet } from 'react-helmet';
import { Container } from '@openedx/paragon';
import { StudioFooterSlot } from '@edx/frontend-component-footer';
@@ -23,9 +24,12 @@ const AccessibilityPage = () => {
</title>
</Helmet>
<Header isHiddenMainMenu />
<Container size="xl" classNamae="px-4">
<Container size="xl" className="px-4">
<AccessibilityBody
{...{ email: ACCESSIBILITY_EMAIL, communityAccessibilityLink: COMMUNITY_ACCESSIBILITY_LINK }}
{...{
email: ACCESSIBILITY_EMAIL,
communityAccessibilityLink: getExternalLinkUrl(COMMUNITY_ACCESSIBILITY_LINK),
}}
/>
<AccessibilityForm accessibilityEmail={ACCESSIBILITY_EMAIL} />
</Container>

View File

@@ -2191,7 +2191,7 @@ describe('<CourseUnit />', () => {
const currentSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
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';
const helpLinkUrl = 'https://docs.openedx.org/en/latest/educators/references/course_development/parent_child_components.html';
await waitFor(() => {
const unitHeaderTitle = screen.getByTestId('unit-header-title');

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Card, Hyperlink, Stack } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getExternalLinkUrl } from '@edx/frontend-platform';
import messages from './messages';
@@ -44,7 +45,7 @@ const SplitTestSidebarInfo = () => {
<hr className="course-split-test-sidebar-devider my-4" />
<Hyperlink
showLaunchIcon={false}
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"
destination={getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/references/course_development/parent_child_components.html')}
className="btn btn-outline-primary btn-sm"
target="_blank"
>

View File

@@ -12,6 +12,7 @@ import {
} from '@openedx/paragon';
import { ArrowBack } from '@openedx/paragon/icons';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { getExternalLinkUrl } from '@edx/frontend-platform';
import {
AdvancedProblemType,
AdvanceProblems,
@@ -91,7 +92,7 @@ const AdvanceTypeSelect: React.FC<Props> = ({
</Form.RadioSet>
</Form.Group>
<Hyperlink
destination="https://docs.openedx.org/en/latest/educators/references/course_development/exercise_tools/guide_problem_types.html#advanced-problem-types"
destination={getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/references/course_development/exercise_tools/guide_problem_types.html#advanced-problem-types')}
target="_blank"
>
<FormattedMessage {...messages.learnMoreAdvancedButtonLabel} />

View File

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import {
FormattedMessage,
} from '@edx/frontend-platform/i18n';
import { getExternalLinkUrl } from '@edx/frontend-platform';
import {
Stack,
Hyperlink,
@@ -30,7 +31,7 @@ const LicenseDisplay = ({
{license === LicenseTypes.creativeCommons && (
<Hyperlink
className="text-primary-500 x-small"
destination="https://creativecommons.org/about"
destination={getExternalLinkUrl('https://creativecommons.org/about')}
target="_blank"
>
<FormattedMessage {...messages.viewLicenseDetailsLabel} />

View File

@@ -5,6 +5,7 @@ import {
FormattedMessage,
useIntl,
} from '@edx/frontend-platform/i18n';
import { getExternalLinkUrl } from '@edx/frontend-platform';
import {
Hyperlink,
Form,
@@ -30,7 +31,7 @@ const SocialShareWidget = ({
const intl = useIntl();
const isSetByCourse = allowVideoSharing.level === 'course';
const videoSharingEnabled = isLibrary ? videoSharingEnabledForAll : videoSharingEnabledForCourse;
const learnMoreLink = videoSharingLearnMoreLink || 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html';
const learnMoreLink = videoSharingLearnMoreLink || getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html');
const onSocialSharingCheckboxChange = hooks.useTrackSocialSharingChange({ updateField });
const getSubtitle = () => {

View File

@@ -1,3 +1,4 @@
import { getExternalLinkUrl } from '@edx/frontend-platform';
import { StrictDict } from '../../utils';
import singleSelect from '../images/singleSelect.png';
import multiSelect from '../images/multiSelect.png';
@@ -41,7 +42,7 @@ export const getProblemTypes = (formatMessage) => ({
preview: singleSelect,
previewDescription: formatMessage(problemMessages.singleSelectDescription),
description: formatMessage(problemMessages.singleSelectInstruction),
helpLink: 'https://docs.openedx.org/en/latest/educators/concepts/exercise_tools/about_multi_select.html',
helpLink: getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/concepts/exercise_tools/about_multi_select.html'),
prev: ProblemTypeKeys.TEXTINPUT,
next: ProblemTypeKeys.MULTISELECT,
template: basicProblemTemplates.singleSelect.olx,
@@ -52,7 +53,7 @@ export const getProblemTypes = (formatMessage) => ({
preview: multiSelect,
previewDescription: formatMessage(problemMessages.multiSelectDescription),
description: formatMessage(problemMessages.multiSelectInstruction),
helpLink: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_multi_select.html',
helpLink: getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_multi_select.html'),
next: ProblemTypeKeys.DROPDOWN,
prev: ProblemTypeKeys.SINGLESELECT,
template: basicProblemTemplates.multiSelect.olx,
@@ -63,7 +64,7 @@ export const getProblemTypes = (formatMessage) => ({
preview: dropdown,
previewDescription: formatMessage(problemMessages.dropdownDescription),
description: formatMessage(problemMessages.dropdownInstruction),
helpLink: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_dropdown.html',
helpLink: getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_dropdown.html'),
next: ProblemTypeKeys.NUMERIC,
prev: ProblemTypeKeys.MULTISELECT,
template: basicProblemTemplates.dropdown.olx,
@@ -74,7 +75,7 @@ export const getProblemTypes = (formatMessage) => ({
preview: numericalInput,
previewDescription: formatMessage(problemMessages.numericalInputDescription),
description: formatMessage(problemMessages.numericalInputInstruction),
helpLink: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/manage_numerical_input_problem.html',
helpLink: getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/manage_numerical_input_problem.html'),
next: ProblemTypeKeys.TEXTINPUT,
prev: ProblemTypeKeys.DROPDOWN,
template: basicProblemTemplates.numeric.olx,
@@ -85,7 +86,7 @@ export const getProblemTypes = (formatMessage) => ({
preview: textInput,
previewDescription: formatMessage(problemMessages.textInputDescription),
description: formatMessage(problemMessages.textInputInstruction),
helpLink: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_text_input.html',
helpLink: getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_text_input.html'),
prev: ProblemTypeKeys.NUMERIC,
next: ProblemTypeKeys.SINGLESELECT,
template: basicProblemTemplates.textInput.olx,
@@ -105,7 +106,7 @@ export const ProblemTypes = StrictDict({
preview: singleSelect,
previewDescription: 'Learners must select the correct answer from a list of possible options.',
description: 'Enter your single select answers below and select which choices are correct. Learners must choose one correct answer.',
helpLink: 'https://docs.openedx.org/en/latest/educators/concepts/exercise_tools/about_multi_select.html',
helpLink: getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/concepts/exercise_tools/about_multi_select.html'),
prev: ProblemTypeKeys.TEXTINPUT,
next: ProblemTypeKeys.MULTISELECT,
template: basicProblemTemplates.singleSelect.olx,
@@ -116,7 +117,7 @@ export const ProblemTypes = StrictDict({
preview: multiSelect,
previewDescription: 'Learners must select all correct answers from a list of possible options.',
description: 'Enter your multi select answers below and select which choices are correct. Learners must choose all correct answers.',
helpLink: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_multi_select.html',
helpLink: getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_multi_select.html'),
next: ProblemTypeKeys.DROPDOWN,
prev: ProblemTypeKeys.SINGLESELECT,
template: basicProblemTemplates.multiSelect.olx,
@@ -127,7 +128,7 @@ export const ProblemTypes = StrictDict({
preview: dropdown,
previewDescription: 'Learners must select the correct answer from a list of possible options',
description: 'Enter your dropdown answers below and select which choice is correct. Learners must select one correct answer.',
helpLink: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_dropdown.html',
helpLink: getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_dropdown.html'),
next: ProblemTypeKeys.NUMERIC,
prev: ProblemTypeKeys.MULTISELECT,
template: basicProblemTemplates.dropdown.olx,
@@ -138,7 +139,7 @@ export const ProblemTypes = StrictDict({
preview: numericalInput,
previewDescription: 'Specify one or more correct numeric answers, submitted in a response field.',
description: 'Enter correct numerical input answers below. Learners must enter one correct answer.',
helpLink: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/manage_numerical_input_problem.html',
helpLink: getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/manage_numerical_input_problem.html'),
next: ProblemTypeKeys.TEXTINPUT,
prev: ProblemTypeKeys.DROPDOWN,
template: basicProblemTemplates.numeric.olx,
@@ -149,7 +150,7 @@ export const ProblemTypes = StrictDict({
preview: textInput,
previewDescription: 'Specify one or more correct text answers, including numbers and special characters, submitted in a response field.',
description: 'Enter your text input answers below and select which choices are correct. Learners must enter one correct answer.',
helpLink: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_text_input.html',
helpLink: getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_text_input.html'),
prev: ProblemTypeKeys.NUMERIC,
next: ProblemTypeKeys.SINGLESELECT,
template: basicProblemTemplates.textInput.olx,

View File

@@ -1,5 +1,6 @@
import { Alert, Button, Hyperlink } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { getExternalLinkUrl } from '@edx/frontend-platform';
import { useNavigate } from 'react-router-dom';
import { useLibrariesV1Data } from '@src/studio-home/data/apiHooks';
@@ -10,7 +11,7 @@ const libraryDocsLink = (
<Hyperlink
target="_blank"
showLaunchIcon={false}
destination="https://docs.openedx.org/en/latest/educators/how-tos/course_development/create_new_library.html"
destination={getExternalLinkUrl('https://docs.openedx.org/en/latest/educators/how-tos/course_development/create_new_library.html')}
>
<FormattedMessage {...messages.alertLibrariesDocLinkText} />
</Hyperlink>