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); + } + }} + > + + + + + )} />