diff --git a/src/course-outline/unit-card/UnitCard.scss b/src/course-outline/unit-card/UnitCard.scss
index 67ab80f407..2a3eba6993 100644
--- a/src/course-outline/unit-card/UnitCard.scss
+++ b/src/course-outline/unit-card/UnitCard.scss
@@ -25,13 +25,34 @@
display: flex;
align-items: center;
- >span.flex-grow-1 {
+ >.flex-grow-1 {
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 80%;
}
+
+ >a.flex-grow-1 {
+ color: inherit;
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ >a.component-card-button-icon {
+ text-decoration: none;
+ color: var(--pgn-color-primary-500);
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ color: var(--pgn-color-white);
+ background-color: var(--pgn-color-primary-500);
+ }
+ }
}
}
diff --git a/src/course-outline/unit-card/UnitCard.test.tsx b/src/course-outline/unit-card/UnitCard.test.tsx
index c6e2ed93fb..769a732f92 100644
--- a/src/course-outline/unit-card/UnitCard.test.tsx
+++ b/src/course-outline/unit-card/UnitCard.test.tsx
@@ -1,6 +1,7 @@
import {
act, fireEvent, initializeMocks, render, screen, waitFor, within,
} from '@src/testUtils';
+import { getConfig } from '@edx/frontend-platform';
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
import { XBlock } from '@src/data/types';
@@ -262,4 +263,82 @@ describe('', () => {
expect(await screen.findByText(/unable to load unit components/i)).toBeInTheDocument();
expect(screen.queryByText(errorMessage)).not.toBeInTheDocument();
});
+
+ describe('component editor links', () => {
+ const htmlComponent = {
+ blockId: 'block-v1:test+type@html+block@1',
+ blockType: 'html',
+ displayName: 'HTML Component',
+ };
+ const oraComponent = {
+ blockId: 'block-v1:test+type@openassessment+block@2',
+ blockType: 'openassessment',
+ displayName: 'ORA Component',
+ };
+
+ const setupExpandedView = async (components: any[]) => {
+ mockUseUnitHandler.mockReturnValue({
+ data: { components },
+ isLoading: false,
+ isError: false,
+ error: null,
+ });
+
+ renderComponent();
+
+ const expandButton = await screen.findByTestId('unit-card-header__expanded-btn');
+ fireEvent.click(expandButton);
+ };
+
+ it('renders component names as links to the unit page', async () => {
+ await setupExpandedView([htmlComponent]);
+
+ const link = await screen.findByTestId('component-name-link');
+ expect(link.tagName).toBe('A');
+ expect(link).toHaveAttribute('href', `/some/${unit.id}#${htmlComponent.blockId}`);
+ expect(link).toHaveTextContent('HTML Component');
+ });
+
+ it('renders edit button with correct href for MFE-supported types', async () => {
+ await setupExpandedView([htmlComponent]);
+
+ const editButton = await screen.findByTestId('component-edit-button');
+ expect(editButton.tagName).toBe('A');
+ expect(editButton).toHaveAttribute('href', `/course/5/editor/html/${htmlComponent.blockId}`);
+ });
+
+ it('renders edit button with legacy Studio URL for non-MFE types', async () => {
+ await setupExpandedView([oraComponent]);
+
+ const editButton = await screen.findByTestId('component-edit-button');
+ const returnTo = encodeURIComponent(`${getConfig().STUDIO_BASE_URL}/container/${unit.id}`);
+ expect(editButton).toHaveAttribute(
+ 'href',
+ `${getConfig().STUDIO_BASE_URL}/xblock/${oraComponent.blockId}/action/edit?returnTo=${returnTo}`,
+ );
+ });
+
+ it('opens modal editor on plain left-click on edit button (does not navigate)', async () => {
+ await setupExpandedView([htmlComponent]);
+
+ const editButton = await screen.findByTestId('component-edit-button');
+ const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
+ Object.defineProperty(clickEvent, 'metaKey', { value: false });
+ Object.defineProperty(clickEvent, 'ctrlKey', { value: false });
+ Object.defineProperty(clickEvent, 'button', { value: 0 });
+
+ const prevented = !editButton.dispatchEvent(clickEvent);
+ expect(prevented).toBe(true);
+ });
+
+ it('allows Ctrl+click on edit button to open in new tab (does not prevent default)', async () => {
+ await setupExpandedView([htmlComponent]);
+
+ const editButton = await screen.findByTestId('component-edit-button');
+ const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, ctrlKey: true });
+
+ const prevented = !editButton.dispatchEvent(clickEvent);
+ expect(prevented).toBe(false);
+ });
+ });
});
diff --git a/src/course-outline/unit-card/UnitCard.tsx b/src/course-outline/unit-card/UnitCard.tsx
index 150e7d936a..28f18e211d 100644
--- a/src/course-outline/unit-card/UnitCard.tsx
+++ b/src/course-outline/unit-card/UnitCard.tsx
@@ -7,7 +7,9 @@ import {
useState,
} from 'react';
import { useDispatch } from 'react-redux';
-import { useToggle, Icon, IconButtonWithTooltip } from '@openedx/paragon';
+import {
+ useToggle, Icon, OverlayTrigger, Tooltip,
+} from '@openedx/paragon';
import { EditOutline as EditIcon } from '@openedx/paragon/icons';
import { isEmpty } from 'lodash';
import { useParams, useSearchParams } from 'react-router-dom';
@@ -211,6 +213,14 @@ const UnitCard = ({
const supportsMFEEditor = (blockType: string): boolean => Boolean(supportedEditors[blockType]);
+ const getComponentEditorUrl = (blockType: string, blockId: string): string => {
+ if (supportsMFEEditor(blockType)) {
+ return `/course/${courseId}/editor/${blockType}/${blockId}`;
+ }
+ const returnTo = encodeURIComponent(`${getConfig().STUDIO_BASE_URL}/container/${id}`);
+ return `${getConfig().STUDIO_BASE_URL}/xblock/${blockId}/action/edit?returnTo=${returnTo}`;
+ };
+
const handleShowLegacyEditModal = (blockId: string) => {
setEditXBlockId(blockId);
setShowLegacyEditModal(true);
@@ -477,6 +487,13 @@ const UnitCard = ({
id={component.blockId}
key={component.blockId}
buttonVariant="secondary"
+ isClickable
+ onClick={() => handleComponentClick(component.blockId)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ handleComponentClick(component.blockId);
+ }
+ }}
componentStyle={{
background: 'white',
borderRadius: '6px',
@@ -487,28 +504,49 @@ const UnitCard = ({
borderRadius: '6px 6px 0px 0px',
padding: '12px 16px',
}}
- isClickable
- onClick={() => handleComponentClick(component.blockId)}
- onKeyDown={(e) => {
- if (e.key === 'Enter') {
- handleComponentClick(component.blockId);
- }
- }}
actions={(
<>
- {component.displayName}
- {intl.formatMessage(messages.editComponent)}}
- iconAs={EditIcon}
+ {
e.stopPropagation();
- handleComponentEdit(e, component.blockType, component.blockId);
+ if (!e.metaKey && !e.ctrlKey) {
+ e.preventDefault();
+ handleComponentClick(component.blockId);
+ }
}}
- />
+ >
+ {component.displayName}
+
+
+ {intl.formatMessage(messages.editComponent)}
+
+ )}
+ >
+ {
+ e.stopPropagation();
+ if (!e.metaKey && !e.ctrlKey) {
+ e.preventDefault();
+ handleComponentEdit(e, component.blockType, component.blockId);
+ }
+ }}
+ >
+
+
+
+
+
>
)}
/>