From e111d9ba6ea2469d3e80d6c3cfe4f236c2b1dd1a Mon Sep 17 00:00:00 2001 From: Devasia Joseph Date: Tue, 24 Feb 2026 09:38:17 +0530 Subject: [PATCH 1/2] feat: open component in a new tab --- src/course-outline/unit-card/UnitCard.scss | 11 ++- .../unit-card/UnitCard.test.tsx | 70 +++++++++++++++++++ src/course-outline/unit-card/UnitCard.tsx | 24 ++++++- 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/course-outline/unit-card/UnitCard.scss b/src/course-outline/unit-card/UnitCard.scss index 67ab80f407..8576a40de7 100644 --- a/src/course-outline/unit-card/UnitCard.scss +++ b/src/course-outline/unit-card/UnitCard.scss @@ -25,13 +25,22 @@ 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; + } + } } } diff --git a/src/course-outline/unit-card/UnitCard.test.tsx b/src/course-outline/unit-card/UnitCard.test.tsx index c6e2ed93fb..75b6788fc4 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,73 @@ 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 with correct href for MFE-supported types', async () => { + await setupExpandedView([htmlComponent]); + + const link = await screen.findByTestId('component-editor-link'); + expect(link.tagName).toBe('A'); + expect(link).toHaveAttribute('href', `/course/5/editor/html/${htmlComponent.blockId}`); + expect(link).toHaveTextContent('HTML Component'); + }); + + it('renders component names as links with legacy Studio URL for non-MFE types', async () => { + await setupExpandedView([oraComponent]); + + const link = await screen.findByTestId('component-editor-link'); + expect(link).toHaveAttribute( + 'href', + `${getConfig().STUDIO_BASE_URL}/xblock/${oraComponent.blockId}/action/edit`, + ); + }); + + it('opens modal editor on plain left-click (does not navigate)', async () => { + await setupExpandedView([htmlComponent]); + + const link = await screen.findByTestId('component-editor-link'); + 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 = !link.dispatchEvent(clickEvent); + expect(prevented).toBe(true); + }); + + it('allows Ctrl+click to open in new tab (does not prevent default)', async () => { + await setupExpandedView([htmlComponent]); + + const link = await screen.findByTestId('component-editor-link'); + const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, ctrlKey: true }); + + const prevented = !link.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..2f31dc187d 100644 --- a/src/course-outline/unit-card/UnitCard.tsx +++ b/src/course-outline/unit-card/UnitCard.tsx @@ -211,6 +211,13 @@ 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}`; + } + return `${getConfig().STUDIO_BASE_URL}/xblock/${blockId}/action/edit`; + }; + const handleShowLegacyEditModal = (blockId: string) => { setEditXBlockId(blockId); setShowLegacyEditModal(true); @@ -497,7 +504,22 @@ const UnitCard = ({ actions={( <> - {component.displayName} + { + if (e.metaKey || e.ctrlKey) { + e.stopPropagation(); + return; + } + e.preventDefault(); + e.stopPropagation(); + handleComponentEdit(e, component.blockType, component.blockId); + }} + > + {component.displayName} + Date: Tue, 10 Mar 2026 19:49:49 +0530 Subject: [PATCH 2/2] feat: support opening component editor in a new tab via Ctrl/Cmd+click --- src/course-outline/unit-card/UnitCard.scss | 12 ++++ .../unit-card/UnitCard.test.tsx | 35 +++++---- src/course-outline/unit-card/UnitCard.tsx | 72 +++++++++++-------- 3 files changed, 78 insertions(+), 41 deletions(-) diff --git a/src/course-outline/unit-card/UnitCard.scss b/src/course-outline/unit-card/UnitCard.scss index 8576a40de7..2a3eba6993 100644 --- a/src/course-outline/unit-card/UnitCard.scss +++ b/src/course-outline/unit-card/UnitCard.scss @@ -41,6 +41,18 @@ 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 75b6788fc4..769a732f92 100644 --- a/src/course-outline/unit-card/UnitCard.test.tsx +++ b/src/course-outline/unit-card/UnitCard.test.tsx @@ -290,45 +290,54 @@ describe('', () => { fireEvent.click(expandButton); }; - it('renders component names as links with correct href for MFE-supported types', async () => { + it('renders component names as links to the unit page', async () => { await setupExpandedView([htmlComponent]); - const link = await screen.findByTestId('component-editor-link'); + const link = await screen.findByTestId('component-name-link'); expect(link.tagName).toBe('A'); - expect(link).toHaveAttribute('href', `/course/5/editor/html/${htmlComponent.blockId}`); + expect(link).toHaveAttribute('href', `/some/${unit.id}#${htmlComponent.blockId}`); expect(link).toHaveTextContent('HTML Component'); }); - it('renders component names as links with legacy Studio URL for non-MFE types', async () => { + 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 link = await screen.findByTestId('component-editor-link'); - expect(link).toHaveAttribute( + 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`, + `${getConfig().STUDIO_BASE_URL}/xblock/${oraComponent.blockId}/action/edit?returnTo=${returnTo}`, ); }); - it('opens modal editor on plain left-click (does not navigate)', async () => { + it('opens modal editor on plain left-click on edit button (does not navigate)', async () => { await setupExpandedView([htmlComponent]); - const link = await screen.findByTestId('component-editor-link'); + 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 = !link.dispatchEvent(clickEvent); + const prevented = !editButton.dispatchEvent(clickEvent); expect(prevented).toBe(true); }); - it('allows Ctrl+click to open in new tab (does not prevent default)', async () => { + it('allows Ctrl+click on edit button to open in new tab (does not prevent default)', async () => { await setupExpandedView([htmlComponent]); - const link = await screen.findByTestId('component-editor-link'); + const editButton = await screen.findByTestId('component-edit-button'); const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, ctrlKey: true }); - const prevented = !link.dispatchEvent(clickEvent); + 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 2f31dc187d..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'; @@ -215,7 +217,8 @@ const UnitCard = ({ if (supportsMFEEditor(blockType)) { return `/course/${courseId}/editor/${blockType}/${blockId}`; } - return `${getConfig().STUDIO_BASE_URL}/xblock/${blockId}/action/edit`; + const returnTo = encodeURIComponent(`${getConfig().STUDIO_BASE_URL}/container/${id}`); + return `${getConfig().STUDIO_BASE_URL}/xblock/${blockId}/action/edit?returnTo=${returnTo}`; }; const handleShowLegacyEditModal = (blockId: string) => { @@ -484,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', @@ -494,43 +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={( <> { - if (e.metaKey || e.ctrlKey) { - e.stopPropagation(); - return; - } - e.preventDefault(); e.stopPropagation(); - handleComponentEdit(e, component.blockType, component.blockId); + if (!e.metaKey && !e.ctrlKey) { + e.preventDefault(); + handleComponentClick(component.blockId); + } }} > {component.displayName} - {intl.formatMessage(messages.editComponent)}} - iconAs={EditIcon} - onClick={(e) => { - e.stopPropagation(); - handleComponentEdit(e, component.blockType, component.blockId); - }} - /> + + {intl.formatMessage(messages.editComponent)} + + )} + > + { + e.stopPropagation(); + if (!e.metaKey && !e.ctrlKey) { + e.preventDefault(); + handleComponentEdit(e, component.blockType, component.blockId); + } + }} + > + + + + + )} />