feat: Make sections/subsections/units selectable in course outline [FC-0114] (#2732)

This commit is contained in:
Chris Chávez
2026-01-12 21:12:46 -05:00
committed by GitHub
parent 969f7a2858
commit 4cda17e046
18 changed files with 302 additions and 86 deletions

View File

@@ -1,6 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import {
Container,
Row,
@@ -69,6 +68,7 @@ import OutlineAddChildButtons from './OutlineAddChildButtons';
import { OutlineSidebarProvider } from './outline-sidebar/OutlineSidebarContext';
import { StatusBar } from './status-bar/StatusBar';
import { LegacyStatusBar } from './status-bar/LegacyStatusBar';
import { isOutlineNewDesignEnabled } from './utils';
const CourseOutline = () => {
const intl = useIntl();
@@ -148,7 +148,7 @@ const CourseOutline = () => {
// Show the new actions bar if it is enabled in the configuration.
// This is a temporary flag until the new design feature is fully implemented.
const showNewActionsBar = getConfig().ENABLE_COURSE_OUTLINE_NEW_DESIGN?.toString().toLowerCase() === 'true';
const showNewActionsBar = isOutlineNewDesignEnabled();
// Use `setToastMessage` to show the toast.
const [toastMessage, setToastMessage] = useState<string | null>(null);

View File

@@ -9,6 +9,7 @@ import messages from './messages';
interface NewChildButtonsProps {
handleNewButtonClick: () => void;
handleUseFromLibraryClick: () => void;
onClickCard?: (e: React.MouseEvent) => void;
childType: ContainerType;
btnVariant?: string;
btnClasses?: string;
@@ -18,6 +19,7 @@ interface NewChildButtonsProps {
const OutlineAddChildButtons = ({
handleNewButtonClick,
handleUseFromLibraryClick,
onClickCard,
childType,
btnVariant = 'outline-primary',
btnClasses = 'mt-4 border-gray-500 rounded-0',
@@ -59,7 +61,7 @@ const OutlineAddChildButtons = ({
}
return (
<Stack direction="horizontal" gap={3}>
<Stack direction="horizontal" gap={3} onClick={onClickCard}>
<Button
className={btnClasses}
variant={btnVariant}

View File

@@ -11,6 +11,7 @@ import {
Icon,
IconButton,
IconButtonWithTooltip,
Stack,
useToggle,
} from '@openedx/paragon';
import {
@@ -48,6 +49,7 @@ interface CardHeaderProps {
onClickMoveUp: () => void;
onClickMoveDown: () => void;
onClickCopy?: () => void;
onClickCard?: (e: React.MouseEvent) => void;
titleComponent: ReactNode;
namePrefix: string;
proctoringExamConfigurationLink?: string,
@@ -90,6 +92,7 @@ const CardHeader = ({
onClickMoveUp,
onClickMoveDown,
onClickCopy,
onClickCard,
titleComponent,
namePrefix,
actions,
@@ -154,10 +157,16 @@ const CardHeader = ({
return (
<>
<div
{
/* This is a special case; we can skip accessibility here (tabbing and select with keyboard) since the
`SortableItem` component handles that for the whole `{Container}Card`.
This `onClick` allows the user to select the Card by clicking on white areas of this component. */
}
<div // eslint-disable-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
className="item-card-header"
data-testid={`${namePrefix}-card-header`}
ref={cardHeaderRef}
onClick={onClickCard}
>
{isFormOpen ? (
<Form.Group className="m-0 w-75">
@@ -178,7 +187,7 @@ const CardHeader = ({
/>
</Form.Group>
) : (
<>
<Stack direction="horizontal" gap={2}>
{titleComponent}
<IconButtonWithTooltip
className="item-card-button-icon"
@@ -190,7 +199,7 @@ const CardHeader = ({
// @ts-ignore
disabled={isSaving}
/>
</>
</Stack>
)}
<div className="ml-auto d-flex">
{(isVertical || isSequential) && (

View File

@@ -1,8 +1,7 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Button,
OverlayTrigger,
Tooltip,
IconButtonWithTooltip,
Stack,
} from '@openedx/paragon';
import {
ArrowDropDown as ArrowDownIcon,
@@ -29,32 +28,23 @@ const TitleButton = ({
const titleTooltipMessage = intl.formatMessage(messages.expandTooltip);
return (
<OverlayTrigger
placement="bottom"
overlay={(
<Tooltip
id={`${title}-${titleTooltipMessage}`}
>
{titleTooltipMessage}
</Tooltip>
)}
>
<Button
iconBefore={isExpanded ? ArrowDownIcon : ArrowRightIcon}
variant="tertiary"
<Stack direction="horizontal">
<IconButtonWithTooltip
src={isExpanded ? ArrowDownIcon : ArrowRightIcon}
data-testid={`${namePrefix}-card-header__expanded-btn`}
alt={title}
tooltipContent={<div>{titleTooltipMessage}</div>}
className="item-card-header__title-btn"
onClick={onTitleClick}
title={title}
>
<div className="mr-2">
{prefixIcon}
</div>
<span className={`${namePrefix}-card-title mb-0 truncate-1-line`}>
{title}
</span>
</Button>
</OverlayTrigger>
size="inline"
/>
<div className="mr-2">
{prefixIcon}
</div>
<span className={`${namePrefix}-card-title mb-0 truncate-1-line`}>
{title}
</span>
</Stack>
);
};

View File

@@ -26,7 +26,7 @@ const TitleLink = ({
to={titleLink}
title={title}
>
<span className={`${namePrefix}-card-title mb-0 truncate-1-line text-left`}>
<span className={`${namePrefix}-card-title truncate-1-line mb-0 text-left`}>
{title}
</span>
</Button>

View File

@@ -21,6 +21,7 @@ interface SortableItemProps {
isDraggable?: boolean;
children: React.ReactNode;
componentStyle?: object;
onClick?: (e: React.MouseEvent) => void;
}
const SortableItem = ({
@@ -30,6 +31,7 @@ const SortableItem = ({
componentStyle,
data,
children,
onClick,
}: SortableItemProps) => {
const intl = useIntl();
const {
@@ -66,8 +68,18 @@ const SortableItem = ({
return (
<Row
ref={setNodeRef}
tabIndex={onClick ? 0 : -1}
style={style}
className="mx-0"
onClick={onClick}
onKeyDown={(e) => {
if (!onClick) { return; }
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick(e);
}
}}
>
<Col className="extend-margin px-0">
{children}

View File

@@ -1,4 +1,3 @@
import { getConfig } from '@edx/frontend-platform';
import { breakpoints } from '@openedx/paragon';
import { useMediaQuery } from 'react-responsive';
@@ -6,10 +5,10 @@ import { Sidebar } from '@src/generic/sidebar';
import OutlineHelpSidebar from './OutlineHelpSidebar';
import { useOutlineSidebarContext } from './OutlineSidebarContext';
import { isOutlineNewDesignEnabled } from '../utils';
const OutlineSideBar = () => {
const isMedium = useMediaQuery({ maxWidth: breakpoints.medium.maxWidth });
const showNewSidebar = getConfig().ENABLE_COURSE_OUTLINE_NEW_DESIGN?.toString().toLowerCase() === 'true';
const {
currentPageKey,
@@ -20,7 +19,7 @@ const OutlineSideBar = () => {
} = useOutlineSidebarContext();
// Returns the previous help sidebar component if the waffle flag is disabled
if (!showNewSidebar) {
if (!isOutlineNewDesignEnabled()) {
// On screens smaller than medium, the help sidebar is shown below the course outline
const colSpan = isMedium ? 'col-12' : 'col-3';
return (

View File

@@ -15,6 +15,7 @@ import { OutlineInfoSidebar } from './OutlineInfoSidebar';
import messages from './messages';
import { AddSidebar } from './AddSidebar';
import { isOutlineNewDesignEnabled } from '../utils';
export type OutlineSidebarPageKeys = 'help' | 'info' | 'add';
export type OutlineSidebarPages = Record<OutlineSidebarPageKeys, SidebarPage>;
@@ -26,6 +27,8 @@ interface OutlineSidebarContextData {
open: () => void;
toggle: () => void;
sidebarPages: OutlineSidebarPages;
selectedContainerId?: string;
openContainerInfoSidebar: (containerId: string) => void;
}
const OutlineSidebarContext = createContext<OutlineSidebarContextData | undefined>(undefined);
@@ -36,6 +39,14 @@ export const OutlineSidebarProvider = ({ children }: { children?: React.ReactNod
const [currentPageKey, setCurrentPageKeyState] = useState<OutlineSidebarPageKeys>('info');
const [isOpen, open, , toggle] = useToggle(true);
const [selectedContainerId, setSelectedContainerId] = useState<string | undefined>();
const openContainerInfoSidebar = useCallback((containerId: string) => {
if (isOutlineNewDesignEnabled()) {
setSelectedContainerId(containerId);
}
}, [setSelectedContainerId]);
const setCurrentPageKey = useCallback((pageKey: OutlineSidebarPageKeys) => {
setCurrentPageKeyState(pageKey);
open();
@@ -68,6 +79,8 @@ export const OutlineSidebarProvider = ({ children }: { children?: React.ReactNod
isOpen,
open,
toggle,
selectedContainerId,
openContainerInfoSidebar,
}),
[
currentPageKey,
@@ -76,6 +89,8 @@ export const OutlineSidebarProvider = ({ children }: { children?: React.ReactNod
isOpen,
open,
toggle,
selectedContainerId,
openContainerInfoSidebar,
],
);

View File

@@ -1,8 +1,10 @@
import { getConfig, setConfig } from '@edx/frontend-platform';
import {
act, fireEvent, initializeMocks, render, screen, waitFor, within,
} from '@src/testUtils';
import { XBlock } from '@src/data/types';
import SectionCard from './SectionCard';
import { OutlineSidebarProvider } from '../outline-sidebar/OutlineSidebarContext';
const mockUseAcceptLibraryBlockChanges = jest.fn();
const mockUseIgnoreLibraryBlockChanges = jest.fn();
@@ -116,6 +118,7 @@ const renderComponent = (props?: object, entry = '/course/:courseId') => render(
routerProps: {
initialEntries: [entry],
},
extraWrapper: OutlineSidebarProvider,
},
);
@@ -129,6 +132,32 @@ describe('<SectionCard />', () => {
expect(screen.getByTestId('section-card-header')).toBeInTheDocument();
expect(screen.getByTestId('section-card__content')).toBeInTheDocument();
// The card is not selected
const card = screen.getByTestId('section-card');
expect(card).not.toHaveClass('outline-card-selected');
});
it('render SectionCard component in selected state', () => {
setConfig({
...getConfig(),
ENABLE_COURSE_OUTLINE_NEW_DESIGN: 'true',
});
const { container } = renderComponent();
expect(screen.getByTestId('section-card-header')).toBeInTheDocument();
// The card is not selected
const card = screen.getByTestId('section-card');
expect(card).not.toHaveClass('outline-card-selected');
// Get the <Row> that contains the card and click it to select the card
const el = container.querySelector('div.row.mx-0') as HTMLInputElement;
expect(el).not.toBeNull();
fireEvent.click(el!);
// The card is selected
expect(card).toHaveClass('outline-card-selected');
});
it('expands/collapses the card when the expand button is clicked', () => {

View File

@@ -30,6 +30,7 @@ import type { XBlock } from '@src/data/types';
import { invalidateLinksQuery } from '@src/course-libraries/data/apiHooks';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
import messages from './messages';
import { useOutlineSidebarContext } from '../outline-sidebar/OutlineSidebarContext';
interface SectionCardProps {
section: XBlock,
@@ -74,6 +75,7 @@ const SectionCard = ({
const intl = useIntl();
const dispatch = useDispatch();
const { activeId, overId } = useContext(DragContext);
const { selectedContainerId, openContainerInfoSidebar } = useOutlineSidebarContext();
const [searchParams] = useSearchParams();
const locatorId = searchParams.get('show');
const isScrolledToElement = locatorId === section.id;
@@ -266,6 +268,13 @@ const SectionCard = ({
const isDraggable = actions.draggable && (actions.allowMoveUp || actions.allowMoveDown);
const onClickCard = useCallback((e: React.MouseEvent, preventNodeEvents: boolean) => {
if (!preventNodeEvents || e.target === e.currentTarget) {
openContainerInfoSidebar(section.id);
setIsExpanded(true);
}
}, [openContainerInfoSidebar]);
return (
<>
<SortableItem
@@ -281,9 +290,16 @@ const SectionCard = ({
padding: '1.75rem',
...borderStyle,
}}
onClick={(e) => onClickCard(e, true)}
>
<div
className={`section-card ${isScrolledToElement ? 'highlight' : ''}`}
className={classNames(
'section-card',
{
highlight: isScrolledToElement,
'outline-card-selected': section.id === selectedContainerId,
},
)}
data-testid="section-card"
ref={currentRef}
>
@@ -303,6 +319,7 @@ const SectionCard = ({
onClickMoveUp={handleSectionMoveUp}
onClickMoveDown={handleSectionMoveDown}
onClickSync={openSyncModal}
onClickCard={(e) => onClickCard(e, true)}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
@@ -315,7 +332,18 @@ const SectionCard = ({
/>
)}
<div className="section-card__content" data-testid="section-card__content">
<div className="outline-section__status mb-1">
{
/* This is a special case; we can skip accessibility here (tabbing and select with keyboard) since the
`SortableItem` component handles that for the whole `SectionCard`.
This `onClick` allows the user to select the Card by clicking on white areas of this component. */
}
<div // eslint-disable-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
className="outline-section__status mb-1"
onClick={
/* istanbul ignore next */
(e) => onClickCard(e, true)
}
>
<Button
className="p-0 bg-transparent"
data-destid="section-card-highlights-button"
@@ -328,11 +356,23 @@ const SectionCard = ({
<p className="m-0 text-black">{messages.sectionHighlightsBadge.defaultMessage}</p>
</Button>
</div>
<XBlockStatus
isSelfPaced={isSelfPaced}
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
blockData={section}
/>
{
/* This is a special case; we can skip accessibility here (tabbing and select with keyboard) since the
`SortableItem` component handles that for the whole `SectionCard`.
This `onClick` allows the user to select the Card by clicking on white areas of this component. */
}
<div // eslint-disable-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
onClick={
/* istanbul ignore next */
(e) => onClickCard(e, false)
}
>
<XBlockStatus
isSelfPaced={isSelfPaced}
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
blockData={section}
/>
</div>
</div>
{isExpanded && (
<div
@@ -344,6 +384,7 @@ const SectionCard = ({
<OutlineAddChildButtons
handleNewButtonClick={() => handleNewSubsectionSubmit(id)}
handleUseFromLibraryClick={openAddLibrarySubsectionModal}
onClickCard={(e) => onClickCard(e, true)}
childType={ContainerType.Subsection}
/>
)}

View File

@@ -1,3 +1,4 @@
import { getConfig, setConfig } from '@edx/frontend-platform';
import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
import {
act, fireEvent, initializeMocks, render, screen, waitFor, within,
@@ -5,6 +6,7 @@ import {
import { XBlock } from '@src/data/types';
import cardHeaderMessages from '../card-header/messages';
import SubsectionCard from './SubsectionCard';
import { OutlineSidebarProvider } from '../outline-sidebar/OutlineSidebarContext';
let store;
const containerKey = 'lct:org:lib:unit:1';
@@ -141,6 +143,7 @@ const renderComponent = (props?: object, entry = '/course/:courseId') => render(
routerProps: {
initialEntries: [entry],
},
extraWrapper: OutlineSidebarProvider,
},
);
@@ -154,6 +157,32 @@ describe('<SubsectionCard />', () => {
renderComponent();
expect(screen.getByTestId('subsection-card-header')).toBeInTheDocument();
// The card is not selected
const card = screen.getByTestId('subsection-card');
expect(card).not.toHaveClass('outline-card-selected');
});
it('render SubsectionCard component in selected state', () => {
setConfig({
...getConfig(),
ENABLE_COURSE_OUTLINE_NEW_DESIGN: 'true',
});
const { container } = renderComponent();
expect(screen.getByTestId('subsection-card-header')).toBeInTheDocument();
// The card is not selected
const card = screen.getByTestId('subsection-card');
expect(card).not.toHaveClass('outline-card-selected');
// Get the <Row> that contains the card and click it to select the card
const el = container.querySelector('div.row.mx-0') as HTMLInputElement;
expect(el).not.toBeNull();
fireEvent.click(el!);
// The card is selected
expect(card).toHaveClass('outline-card-selected');
});
it('expands/collapses the card when the subsection button is clicked', async () => {

View File

@@ -31,6 +31,7 @@ import type { XBlock } from '@src/data/types';
import { invalidateLinksQuery } from '@src/course-libraries/data/apiHooks';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
import messages from './messages';
import { useOutlineSidebarContext } from '../outline-sidebar/OutlineSidebarContext';
interface SubsectionCardProps {
section: XBlock,
@@ -77,6 +78,7 @@ const SubsectionCard = ({
const intl = useIntl();
const dispatch = useDispatch();
const { activeId, overId } = useContext(DragContext);
const { selectedContainerId, openContainerInfoSidebar } = useOutlineSidebarContext();
const [searchParams] = useSearchParams();
const locatorId = searchParams.get('show');
const isScrolledToElement = locatorId === subsection.id;
@@ -258,6 +260,13 @@ const SubsectionCard = ({
closeAddLibraryUnitModal();
}, [id, handleAddUnitFromLibrary, closeAddLibraryUnitModal]);
const onClickCard = useCallback((e: React.MouseEvent, preventNodeEvents: boolean) => {
if (!preventNodeEvents || e.target === e.currentTarget) {
openContainerInfoSidebar(subsection.id);
setIsExpanded(true);
}
}, [openContainerInfoSidebar]);
return (
<>
<SortableItem
@@ -275,9 +284,16 @@ const SubsectionCard = ({
background: '#f8f7f6',
...borderStyle,
}}
onClick={(e) => onClickCard(e, true)}
>
<div
className={`subsection-card ${isScrolledToElement ? 'highlight' : ''}`}
className={classNames(
'subsection-card',
{
highlight: isScrolledToElement,
'outline-card-selected': subsection.id === selectedContainerId,
},
)}
data-testid="subsection-card"
ref={currentRef}
>
@@ -297,6 +313,7 @@ const SubsectionCard = ({
onClickMoveDown={handleSubsectionMoveDown}
onClickConfigure={onOpenConfigureModal}
onClickSync={openSyncModal}
onClickCard={(e) => onClickCard(e, true)}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
@@ -310,7 +327,19 @@ const SubsectionCard = ({
extraActionsComponent={extraActionsComponent}
readyToSync={upstreamInfo?.readyToSync}
/>
<div className="subsection-card__content item-children" data-testid="subsection-card__content">
{
/* This is a special case; we can skip accessibility here (tabbing and select with keyboard) since the
`SortableItem` component handles that for the whole `SubsectionCard`.
This `onClick` allows the user to select the Card by clicking on white areas of this component. */
}
<div // eslint-disable-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
className="subsection-card__content item-children"
data-testid="subsection-card__content"
onClick={
/* istanbul ignore next */
(e) => onClickCard(e, false)
}
>
<XBlockStatus
isSelfPaced={isSelfPaced}
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
@@ -330,6 +359,7 @@ const SubsectionCard = ({
<OutlineAddChildButtons
handleNewButtonClick={handleNewButtonClick}
handleUseFromLibraryClick={openAddLibraryUnitModal}
onClickCard={(e) => onClickCard(e, true)}
childType={ContainerType.Unit}
/>
{enableCopyPasteUnits && showPasteUnit && sharedClipboardData && (

View File

@@ -11,5 +11,7 @@
line-height: var(--pgn-typography-headings-line-height);
color: var(--pgn-color-headings-base);
align-self: center;
min-width: 10px !important;
max-width: 300px;
}
}

View File

@@ -1,3 +1,4 @@
import { getConfig, setConfig } from '@edx/frontend-platform';
import {
act, fireEvent, initializeMocks, render, screen, waitFor, within,
} from '@src/testUtils';
@@ -5,6 +6,7 @@ import {
import { XBlock } from '@src/data/types';
import UnitCard from './UnitCard';
import cardMessages from '../card-header/messages';
import { OutlineSidebarProvider } from '../outline-sidebar/OutlineSidebarContext';
const mockUseAcceptLibraryBlockChanges = jest.fn();
const mockUseIgnoreLibraryBlockChanges = jest.fn();
@@ -105,6 +107,7 @@ const renderComponent = (props?: object) => render(
{
path: '/course/:courseId',
params: { courseId: '5' },
extraWrapper: OutlineSidebarProvider,
},
);
@@ -121,6 +124,33 @@ describe('<UnitCard />', () => {
'href',
'/some/block-v1:UNIX+UX1+2025_T3+type@unit+block@0',
);
// The card is not selected
const card = screen.getByTestId('unit-card');
expect(card).not.toHaveClass('outline-card-selected');
});
it('render UnitCard component in selected state', () => {
setConfig({
...getConfig(),
ENABLE_COURSE_OUTLINE_NEW_DESIGN: 'true',
});
const { container } = renderComponent();
expect(screen.getByTestId('unit-card-header')).toBeInTheDocument();
// The card is not selected
const card = screen.getByTestId('unit-card');
expect(card).not.toHaveClass('outline-card-selected');
// Get the <Row> that contains the card and click it to select the card
const el = container.querySelector('div.row.mx-0') as HTMLInputElement;
expect(el).not.toBeNull();
fireEvent.click(el!);
// The card is selected
expect(card).toHaveClass('outline-card-selected');
});
it('hides header based on isHeaderVisible flag', async () => {

View File

@@ -4,6 +4,7 @@ import {
useMemo,
useRef,
} from 'react';
import classNames from 'classnames';
import { useDispatch } from 'react-redux';
import { useToggle } from '@openedx/paragon';
import { isEmpty } from 'lodash';
@@ -25,6 +26,7 @@ import { PreviewLibraryXBlockChanges } from '@src/course-unit/preview-changes';
import { invalidateLinksQuery } from '@src/course-libraries/data/apiHooks';
import type { XBlock } from '@src/data/types';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
import { useOutlineSidebarContext } from '../outline-sidebar/OutlineSidebarContext';
interface UnitCardProps {
unit: XBlock;
@@ -69,6 +71,7 @@ const UnitCard = ({
const currentRef = useRef(null);
const dispatch = useDispatch();
const [searchParams] = useSearchParams();
const { selectedContainerId, openContainerInfoSidebar } = useOutlineSidebarContext();
const locatorId = searchParams.get('show');
const isScrolledToElement = locatorId === unit.id;
const [isFormOpen, openForm, closeForm] = useToggle(false);
@@ -164,6 +167,12 @@ const UnitCard = ({
}
}, [dispatch, section, queryClient, courseId]);
const onClickCard = useCallback((e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
openContainerInfoSidebar(unit.id);
}
}, [openContainerInfoSidebar]);
const titleComponent = (
<TitleLink
title={displayName}
@@ -228,9 +237,16 @@ const UnitCard = ({
background: '#fdfdfd',
...borderStyle,
}}
onClick={onClickCard}
>
<div
className={`unit-card ${isScrolledToElement ? 'highlight' : ''}`}
className={classNames(
'unit-card',
{
highlight: isScrolledToElement,
'outline-card-selected': unit.id === selectedContainerId,
},
)}
data-testid="unit-card"
ref={currentRef}
>
@@ -248,6 +264,7 @@ const UnitCard = ({
onClickMoveUp={handleUnitMoveUp}
onClickMoveDown={handleUnitMoveDown}
onClickSync={openSyncModal}
onClickCard={onClickCard}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}

View File

@@ -1,4 +1,5 @@
import type { IntlShape, MessageDescriptor } from 'react-intl';
import { getConfig } from '@edx/frontend-platform';
import {
CheckCircle as CheckCircleIcon,
Lock as LockIcon,
@@ -204,6 +205,13 @@ const getVideoSharingOptionText = (
}
};
/**
* Returns `true` if the new design for the course outline is enabled
*/
const isOutlineNewDesignEnabled = () => (
getConfig().ENABLE_COURSE_OUTLINE_NEW_DESIGN?.toString().toLowerCase() === 'true'
);
export {
getItemStatus,
getItemStatusBadgeContent,
@@ -211,4 +219,5 @@ export {
getHighlightsFormValues,
getVideoSharingOptionText,
scrollToElement,
isOutlineNewDesignEnabled,
};

View File

@@ -39,6 +39,11 @@ div.row:has(> div > div.highlight) {
animation-timing-function: cubic-bezier(1, 0, .72, .04);
}
// To apply selection style to selected Section/Subsecion/Units, in the Course Outline
div.row:has(> div > div.outline-card-selected) {
box-shadow: 0 0 3px 3px var(--pgn-color-primary-500) !important;
}
// To apply the glow effect to the selected xblock, in the Unit Outline
div.xblock-highlight {
animation: 5s glow;

View File

@@ -1,8 +1,8 @@
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { getConfig } from '@edx/frontend-platform';
import HeaderNavigations, { HeaderNavigationsProps } from 'CourseAuthoring/course-outline/header-navigations/HeaderNavigations';
import HeaderActions from 'CourseAuthoring/course-outline/header-navigations/HeaderActions';
import { isOutlineNewDesignEnabled } from '@src/course-outline/utils';
interface CourseOutlineHeaderActionsSlotProps extends HeaderNavigationsProps {
sections: Array<({
@@ -20,44 +20,41 @@ const CourseOutlineHeaderActionsSlot = ({
courseActions,
errors,
sections,
}: CourseOutlineHeaderActionsSlotProps) => {
const showNewActionsBar = getConfig().ENABLE_COURSE_OUTLINE_NEW_DESIGN?.toString().toLowerCase() === 'true';
return (
<PluginSlot
id="org.openedx.frontend.authoring.course_outline_header_actions.v1"
idAliases={['course_outline_header_actions_slot']}
pluginProps={{
isReIndexShow,
isSectionsExpanded,
isDisabledReindexButton,
headerNavigationsActions,
hasSections,
courseActions,
errors,
sections,
}}
>
{showNewActionsBar
? (
<HeaderActions
actions={headerNavigationsActions}
courseActions={courseActions}
errors={errors}
/>
)
: (
<HeaderNavigations
headerNavigationsActions={headerNavigationsActions}
isReIndexShow={isReIndexShow}
isSectionsExpanded={isSectionsExpanded}
isDisabledReindexButton={isDisabledReindexButton}
hasSections={hasSections}
courseActions={courseActions}
errors={errors}
/>
)}
</PluginSlot>
);
};
}: CourseOutlineHeaderActionsSlotProps) => (
<PluginSlot
id="org.openedx.frontend.authoring.course_outline_header_actions.v1"
idAliases={['course_outline_header_actions_slot']}
pluginProps={{
isReIndexShow,
isSectionsExpanded,
isDisabledReindexButton,
headerNavigationsActions,
hasSections,
courseActions,
errors,
sections,
}}
>
{isOutlineNewDesignEnabled()
? (
<HeaderActions
actions={headerNavigationsActions}
courseActions={courseActions}
errors={errors}
/>
)
: (
<HeaderNavigations
headerNavigationsActions={headerNavigationsActions}
isReIndexShow={isReIndexShow}
isSectionsExpanded={isSectionsExpanded}
isDisabledReindexButton={isDisabledReindexButton}
hasSections={hasSections}
courseActions={courseActions}
errors={errors}
/>
)}
</PluginSlot>
);
export default CourseOutlineHeaderActionsSlot;