feat: Unit align sidebar [FC-0114] (#2856)

Implements the align sidebar in the unit page for the unit and components
This commit is contained in:
Chris Chávez
2026-02-13 15:07:53 -05:00
committed by GitHub
parent 72421969d9
commit 4d401a9c22
11 changed files with 182 additions and 30 deletions

View File

@@ -1,4 +1,4 @@
import { initializeMocks, render, screen } from '@src/testUtils';
import { render, screen, initializeMocks } from '@src/testUtils';
import * as CourseAuthoringContext from '@src/CourseAuthoringContext';
import * as CourseDetailsApi from '@src/data/apiHooks';

View File

@@ -1,10 +1,11 @@
import { SchoolOutline } from '@openedx/paragon/icons';
import { ContentTagsDrawer } from '@src/content-tags-drawer';
import { useContentData } from '@src/content-tags-drawer/data/apiHooks';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
import { SidebarTitle } from '@src/generic/sidebar';
import { AlignSidebar } from '@src/generic/sidebar/AlignSidebar';
import { useOutlineSidebarContext } from './OutlineSidebarContext';
/**
* Align sidebar for course or selected containers.
*/
export const OutlineAlignSidebar = () => {
const {
courseId,
@@ -24,20 +25,14 @@ export const OutlineAlignSidebar = () => {
};
return (
<div>
<SidebarTitle
title={
contentData && 'displayName' in contentData
? contentData.displayName
: contentData?.courseDisplayNameWithDefault || ''
}
icon={SchoolOutline}
onBackBtnClick={(sidebarContentId !== courseId) ? handleBack : undefined}
/>
<ContentTagsDrawer
id={sidebarContentId}
variant="component"
/>
</div>
<AlignSidebar
title={
contentData && 'displayName' in contentData
? contentData.displayName
: contentData?.courseDisplayNameWithDefault || ''
}
contentId={sidebarContentId}
onBackBtnClick={(sidebarContentId !== courseId) ? handleBack : undefined}
/>
);
};

View File

@@ -2928,6 +2928,22 @@ describe('<CourseUnit />', () => {
expect(await screen.findByText('Access: 3 Groups')).toBeInTheDocument();
});
it('opens the align sidebar on postMessage event', async () => {
setConfig({
...getConfig(),
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
ENABLE_UNIT_PAGE_NEW_DESIGN: 'true',
});
render(<RootWrapper />);
await screen.findByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
simulatePostMessageEvent(messageTypes.openManageTags, { contentId: blockId });
await screen.findByText('Align');
});
describe('Add sidebar', () => {
let user;

View File

@@ -0,0 +1,39 @@
import { render, screen, initializeMocks } from '@src/testUtils';
import { UnitAlignSidebar } from './UnitAlignSidebar';
import { UnitSidebarProvider } from './UnitSidebarContext';
jest.mock('@src/content-tags-drawer', () => ({
ContentTagsDrawer: jest.fn(({ id, variant }) => (
<div data-testid="content-tags-drawer">
drawer-mock-{id}-{variant}
</div>
)),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({ blockId: 'unit-id-1' }),
}));
const renderComponent = () => render(
<UnitSidebarProvider readOnly={false}>
<UnitAlignSidebar />
</UnitSidebarProvider>,
);
describe('OutlineAlignSidebar', () => {
beforeEach(() => {
initializeMocks();
});
it('renders ContentTagsDrawer with the correct id and variant', () => {
renderComponent();
const drawer = screen.getByTestId('content-tags-drawer');
expect(drawer).toBeInTheDocument();
expect(drawer).toHaveTextContent(
'drawer-mock-unit-id-1-component',
);
});
});

View File

@@ -0,0 +1,36 @@
import { useParams } from 'react-router-dom';
import { useContentData } from '@src/content-tags-drawer/data/apiHooks';
import { AlignSidebar } from '@src/generic/sidebar/AlignSidebar';
import { useCallback } from 'react';
import { useUnitSidebarContext } from './UnitSidebarContext';
/**
* Align sidebar for unit or selected components.
*/
export const UnitAlignSidebar = () => {
const { blockId } = useParams();
const { currentComponentId, setCurrentPageKey } = useUnitSidebarContext();
const sidebarContentId = currentComponentId || blockId;
const {
data: contentData,
} = useContentData(sidebarContentId);
const handleBack = useCallback(() => {
// Set the align sidebar without current component to back
// to unit align sidebar.
setCurrentPageKey('align');
}, [setCurrentPageKey]);
return (
<AlignSidebar
title={
contentData && 'displayName' in contentData
? contentData.displayName : ''
}
contentId={sidebarContentId || ''}
onBackBtnClick={currentComponentId ? handleBack : undefined}
/>
);
};

View File

@@ -3,15 +3,20 @@ import {
} from 'react';
import { SidebarPage } from '@src/generic/sidebar';
import { useToggle } from '@openedx/paragon';
import { useStateWithUrlSearchParam } from '@src/hooks';
export type UnitSidebarPageKeys = 'info' | 'add';
export type UnitSidebarPageKeys = 'info' | 'add' | 'align';
export type UnitSidebarPages = Record<UnitSidebarPageKeys, SidebarPage>;
interface UnitSidebarContextData {
currentPageKey: UnitSidebarPageKeys;
setCurrentPageKey: (pageKey: UnitSidebarPageKeys) => void;
setCurrentPageKey: (pageKey: UnitSidebarPageKeys, componentId?: string) => void;
currentTabKey?: string;
setCurrentTabKey: (tabKey: string | undefined) => void;
// The Id of the component used in the current sidebar page
// The component is not necessarily selected to open a selected sidebar.
// Example: Align sidebar
currentComponentId?: string;
isOpen: boolean;
open: () => void;
toggle: () => void;
@@ -27,14 +32,23 @@ export const UnitSidebarProvider = ({
children?: React.ReactNode,
readOnly: boolean,
}) => {
const [currentPageKey, setCurrentPageKeyState] = useState<UnitSidebarPageKeys>('info');
const [currentPageKey, setCurrentPageKeyState] = useStateWithUrlSearchParam<UnitSidebarPageKeys>(
'info',
'sidebar',
(value: string) => value as UnitSidebarPageKeys,
(value: UnitSidebarPageKeys) => value,
);
const [currentTabKey, setCurrentTabKey] = useState<string>();
const [currentComponentId, setCurrentComponentId] = useState<string>();
const [isOpen, open,, toggle] = useToggle(true);
const setCurrentPageKey = useCallback(/* istanbul ignore next */ (pageKey: UnitSidebarPageKeys) => {
// Reset tab
const setCurrentPageKey = useCallback(/* istanbul ignore next */ (
pageKey: UnitSidebarPageKeys,
componentId?: string,
) => {
setCurrentTabKey(undefined);
setCurrentPageKeyState(pageKey);
setCurrentComponentId(componentId);
open();
}, [open]);
@@ -44,6 +58,7 @@ export const UnitSidebarProvider = ({
setCurrentPageKey,
currentTabKey,
setCurrentTabKey,
currentComponentId,
isOpen,
open,
toggle,
@@ -54,6 +69,7 @@ export const UnitSidebarProvider = ({
setCurrentPageKey,
currentTabKey,
setCurrentTabKey,
currentComponentId,
isOpen,
open,
toggle,

View File

@@ -6,6 +6,11 @@ const messages = defineMessages({
defaultMessage: 'Info',
description: 'Label of the button for the Info sidebar',
},
sidebarButtonAlign: {
id: 'course-authoring.unit-page.sidebar.info.sidebar-button-align',
defaultMessage: 'Align',
description: 'Label of the button for the Align sidebar',
},
sidebarButtonAdd: {
id: 'course-authoring.unit-page.sidebar.add.sidebar-button-add',
defaultMessage: 'Add',

View File

@@ -1,7 +1,9 @@
import { Info, Plus } from '@openedx/paragon/icons';
import { getConfig } from '@edx/frontend-platform';
import { Info, Tag, Plus } from '@openedx/paragon/icons';
import { SidebarPage } from '@src/generic/sidebar';
import messages from './messages';
import { UnitInfoSidebar } from './unit-info/UnitInfoSidebar';
import { UnitAlignSidebar } from './UnitAlignSidebar';
import { AddSidebar } from './AddSidebar';
import { useUnitSidebarContext } from './UnitSidebarContext';
@@ -18,6 +20,7 @@ export type UnitSidebarPages = {
* if you want to use the context in the sidebar pages.
*/
export const useUnitSidebarPages = (): UnitSidebarPages => {
const showAlignSidebar = getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true';
const { readOnly } = useUnitSidebarContext();
return {
info: {
@@ -32,5 +35,12 @@ export const useUnitSidebarPages = (): UnitSidebarPages => {
title: messages.sidebarButtonAdd,
},
}),
...(showAlignSidebar && {
align: {
component: UnitAlignSidebar,
icon: Tag,
title: messages.sidebarButtonAlign,
},
}),
};
};

View File

@@ -39,6 +39,8 @@ import {
AccessManagedXBlockDataTypes,
} from './types';
import { formatAccessManagedXBlockData, getIframeUrl, getLegacyEditModalUrl } from './utils';
import { useUnitSidebarContext } from '../unit-sidebar/UnitSidebarContext';
import { isUnitPageNewDesignEnabled } from '../utils';
const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
courseId,
@@ -51,6 +53,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
}) => {
const intl = useIntl();
const dispatch = useDispatch();
const { setCurrentPageKey } = useUnitSidebarContext();
// Useful to reload iframe
const [iframeKey, setIframeKey] = useState(0);
@@ -175,8 +178,13 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
};
const handleOpenManageTagsModal = (id: string) => {
setConfigureXBlockId(id);
openManageTagsModal();
if (isUnitPageNewDesignEnabled()) {
setCurrentPageKey('align', id);
} else {
// Legacy manage tags modal
setConfigureXBlockId(id);
openManageTagsModal();
}
};
const handleShowProcessingNotification = (variant: string) => {

View File

@@ -285,7 +285,7 @@
}
}
.icon-with-border-chapter {
.icon-with-border-section {
border: 1px solid var(--content-library-container-section-color);
.pgn__icon {
@@ -293,7 +293,7 @@
}
}
.icon-with-border-sequential {
.icon-with-border-subsection {
border: 1px solid var(--content-library-container-subsection-color);
.pgn__icon {
@@ -301,7 +301,7 @@
}
}
.icon-with-border-vertical {
.icon-with-border-unit {
border: 1px solid var(--content-library-container-unit-color);
.pgn__icon {

View File

@@ -0,0 +1,27 @@
import { ContentTagsDrawer } from '@src/content-tags-drawer';
import { SchoolOutline } from '@openedx/paragon/icons';
import { SidebarTitle } from './SidebarTitle';
export interface AlignSidebarProps {
contentId: string;
title: string;
onBackBtnClick?: () => void;
}
/**
* Sidebar that renders Align Sidebar (manage tags sidebar)
* for the given content.
*/
export const AlignSidebar = ({ contentId, title, onBackBtnClick }: AlignSidebarProps) => (
<div>
<SidebarTitle
title={title}
icon={SchoolOutline}
onBackBtnClick={onBackBtnClick}
/>
<ContentTagsDrawer
id={contentId}
variant="component"
/>
</div>
);