Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/course-outline/unit-card/UnitCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
}

Expand Down
79 changes: 79 additions & 0 deletions src/course-outline/unit-card/UnitCard.test.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -262,4 +263,82 @@ describe('<UnitCard />', () => {
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);
});
});
});
72 changes: 55 additions & 17 deletions src/course-outline/unit-card/UnitCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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',
Expand All @@ -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={(
<>
<Icon src={ComponentIcon} className="mr-2 text-dark" />
<span className="flex-grow-1">{component.displayName}</span>
<IconButtonWithTooltip
className="component-card-button-icon btn-icon btn-icon-primary btn-icon-md"
data-testid="component-edit-button"
alt={intl.formatMessage(messages.editComponent)}
tooltipContent={<div>{intl.formatMessage(messages.editComponent)}</div>}
iconAs={EditIcon}
<a
href={`${getTitleLink(id)}#${component.blockId}`}
className="flex-grow-1"
data-testid="component-name-link"
onClick={(e) => {
e.stopPropagation();
handleComponentEdit(e, component.blockType, component.blockId);
if (!e.metaKey && !e.ctrlKey) {
e.preventDefault();
handleComponentClick(component.blockId);
}
}}
/>
>
{component.displayName}
</a>
<OverlayTrigger
placement="top"
overlay={(
<Tooltip id={`edit-tooltip-${component.blockId}`}>
{intl.formatMessage(messages.editComponent)}
</Tooltip>
)}
>
<a
href={getComponentEditorUrl(component.blockType, component.blockId)}
className="component-card-button-icon btn btn-icon btn-icon-primary btn-icon-md"
data-testid="component-edit-button"
aria-label={intl.formatMessage(messages.editComponent)}
onClick={(e) => {
e.stopPropagation();
if (!e.metaKey && !e.ctrlKey) {
e.preventDefault();
handleComponentEdit(e, component.blockType, component.blockId);
}
}}
>
<span className="btn-icon_btn-icon-primary_icon">
<Icon src={EditIcon} />
</span>
</a>
</OverlayTrigger>
</>
)}
/>
Expand Down