From 85e2da25dd1898728ea7f9edc54db47a9f0bf547 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 30 Jan 2026 20:05:08 -0500 Subject: [PATCH 1/2] feat: Align sidebar for the Unit page --- .../outline-sidebar/OutlineAlignSidebar.tsx | 29 +++++++-------- .../unit-sidebar/UnitAlignSidebar.tsx | 28 +++++++++++++++ src/course-unit/unit-sidebar/UnitSidebar.tsx | 4 +-- .../unit-sidebar/UnitSidebarContext.tsx | 25 ++++++++++--- src/course-unit/unit-sidebar/constants.ts | 20 ----------- src/course-unit/unit-sidebar/messages.ts | 5 +++ src/course-unit/unit-sidebar/sidebarPages.ts | 35 +++++++++++++++++++ .../xblock-container-iframe/index.tsx | 12 +++++-- src/generic/sidebar/AlignSidebar.tsx | 25 +++++++++++++ 9 files changed, 138 insertions(+), 45 deletions(-) create mode 100644 src/course-unit/unit-sidebar/UnitAlignSidebar.tsx delete mode 100644 src/course-unit/unit-sidebar/constants.ts create mode 100644 src/course-unit/unit-sidebar/sidebarPages.ts create mode 100644 src/generic/sidebar/AlignSidebar.tsx diff --git a/src/course-outline/outline-sidebar/OutlineAlignSidebar.tsx b/src/course-outline/outline-sidebar/OutlineAlignSidebar.tsx index 73593d0572..23b723f7d4 100644 --- a/src/course-outline/outline-sidebar/OutlineAlignSidebar.tsx +++ b/src/course-outline/outline-sidebar/OutlineAlignSidebar.tsx @@ -1,11 +1,12 @@ -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 { useCourseDetails } from '@src/data/apiHooks'; -import { SidebarTitle } from '@src/generic/sidebar'; import { useOutlineSidebarContext } from './OutlineSidebarContext'; +import { AlignSidebar } from '@src/generic/sidebar/AlignSidebar'; +/** + * Align sidebar for course or selected containers. + */ export const OutlineAlignSidebar = () => { const { courseId } = useCourseAuthoringContext(); const { currentContainerId } = useOutlineSidebarContext(); @@ -21,19 +22,13 @@ export const OutlineAlignSidebar = () => { } = useContentData(currentContainerId); return ( -
- - -
+ ); }; diff --git a/src/course-unit/unit-sidebar/UnitAlignSidebar.tsx b/src/course-unit/unit-sidebar/UnitAlignSidebar.tsx new file mode 100644 index 0000000000..5364ccfc1e --- /dev/null +++ b/src/course-unit/unit-sidebar/UnitAlignSidebar.tsx @@ -0,0 +1,28 @@ +import { useParams } from "react-router-dom"; +import { useContentData } from "@src/content-tags-drawer/data/apiHooks"; +import { AlignSidebar } from "@src/generic/sidebar/AlignSidebar"; +import { useUnitSidebarContext } from "./UnitSidebarContext"; + +/** + * Align sidebar for unit or selected components. + */ +export const UnitAlignSidebar = () => { + const { blockId } = useParams(); + const { currentComponentId } = useUnitSidebarContext(); + + const sidebarContentId = currentComponentId || blockId; + + const { + data: contentData, + } = useContentData(sidebarContentId); + + return ( + + ); +}; diff --git a/src/course-unit/unit-sidebar/UnitSidebar.tsx b/src/course-unit/unit-sidebar/UnitSidebar.tsx index 19a3668718..a866b0bec9 100644 --- a/src/course-unit/unit-sidebar/UnitSidebar.tsx +++ b/src/course-unit/unit-sidebar/UnitSidebar.tsx @@ -2,7 +2,7 @@ import { Sidebar } from '@src/generic/sidebar'; import LegacySidebar, { LegacySidebarProps } from '../legacy-sidebar'; import { useUnitSidebarContext } from './UnitSidebarContext'; import { isUnitPageNewDesignEnabled } from '../utils'; -import { UNIT_SIDEBAR_PAGES } from './constants'; +import { getUnitSidebarPages } from './sidebarPages'; export type UnitSidebarProps = { legacySidebarProps: LegacySidebarProps, @@ -26,7 +26,7 @@ export const UnitSidebar = ({ return ( ; interface UnitSidebarContextData { currentPageKey: UnitSidebarPageKeys; - setCurrentPageKey: (pageKey: UnitSidebarPageKeys) => void; + setCurrentPageKey: (pageKey: UnitSidebarPageKeys, componentId?: string) => void; currentTabKey?: string; setCurrentTabKey: (tabKey: string) => 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; @@ -20,12 +25,22 @@ interface UnitSidebarContextData { const UnitSidebarContext = createContext(undefined); export const UnitSidebarProvider = ({ children }: { children?: React.ReactNode }) => { - const [currentPageKey, setCurrentPageKeyState] = useState('info'); + const [currentPageKey, setCurrentPageKeyState] = useStateWithUrlSearchParam( + 'info', + 'sidebar', + (value: string) => value as UnitSidebarPageKeys, + (value: UnitSidebarPageKeys) => value, + ); const [currentTabKey, setCurrentTabKey] = useState(); + const [currentComponentId, setCurrentComponentId] = useState(); const [isOpen, open,, toggle] = useToggle(true); - const setCurrentPageKey = useCallback(/* istanbul ignore next */ (pageKey: UnitSidebarPageKeys) => { + const setCurrentPageKey = useCallback(/* istanbul ignore next */ ( + pageKey: UnitSidebarPageKeys, + componentId?: string, + ) => { setCurrentPageKeyState(pageKey); + setCurrentComponentId(componentId); open(); }, [open]); @@ -35,6 +50,7 @@ export const UnitSidebarProvider = ({ children }: { children?: React.ReactNode } setCurrentPageKey, currentTabKey, setCurrentTabKey, + currentComponentId, isOpen, open, toggle, @@ -44,6 +60,7 @@ export const UnitSidebarProvider = ({ children }: { children?: React.ReactNode } setCurrentPageKey, currentTabKey, setCurrentTabKey, + currentComponentId, isOpen, open, toggle, diff --git a/src/course-unit/unit-sidebar/constants.ts b/src/course-unit/unit-sidebar/constants.ts deleted file mode 100644 index 8bfbe7e003..0000000000 --- a/src/course-unit/unit-sidebar/constants.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Info } from '@openedx/paragon/icons'; -import { SidebarPage } from '@src/generic/sidebar'; -import messages from './messages'; -import { UnitInfoSidebar } from './unit-info/UnitInfoSidebar'; - -export type UnitSidebarPageKeys = 'info'; - -/** - * Sidebar pages for the unit sidebar - * - * This has been separated from the context to avoid a cyclical import - * if you want to use the context in the sidebar pages. - */ -export const UNIT_SIDEBAR_PAGES: Record = { - info: { - component: UnitInfoSidebar, - icon: Info, - title: messages.sidebarButtonInfo, - }, -}; diff --git a/src/course-unit/unit-sidebar/messages.ts b/src/course-unit/unit-sidebar/messages.ts index 96c3326ce1..0abbcfe275 100644 --- a/src/course-unit/unit-sidebar/messages.ts +++ b/src/course-unit/unit-sidebar/messages.ts @@ -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', + }, }); export default messages; diff --git a/src/course-unit/unit-sidebar/sidebarPages.ts b/src/course-unit/unit-sidebar/sidebarPages.ts new file mode 100644 index 0000000000..55f3945743 --- /dev/null +++ b/src/course-unit/unit-sidebar/sidebarPages.ts @@ -0,0 +1,35 @@ +import { getConfig } from '@edx/frontend-platform'; +import { Info, Tag } from '@openedx/paragon/icons'; +import { SidebarPage } from '@src/generic/sidebar'; +import messages from './messages'; +import { UnitInfoSidebar } from './unit-info/UnitInfoSidebar'; +import { UnitAlignSidebar } from './UnitAlignSidebar'; + +export type UnitSidebarPages = { + info: SidebarPage; + align?: SidebarPage; +}; + +/** + * Sidebar pages for the unit sidebar + * + * This has been separated from the context to avoid a cyclical import + * if you want to use the context in the sidebar pages. + */ +export const getUnitSidebarPages = (): UnitSidebarPages => { + const showAlignSidebar = getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true'; + return { + info: { + component: UnitInfoSidebar, + icon: Info, + title: messages.sidebarButtonInfo, + }, + ...(showAlignSidebar && { + align: { + component: UnitAlignSidebar, + icon: Tag, + title: messages.sidebarButtonAlign, + }, + }), + } +}; diff --git a/src/course-unit/xblock-container-iframe/index.tsx b/src/course-unit/xblock-container-iframe/index.tsx index 5b67b340af..7532174df1 100644 --- a/src/course-unit/xblock-container-iframe/index.tsx +++ b/src/course-unit/xblock-container-iframe/index.tsx @@ -39,12 +39,15 @@ import { AccessManagedXBlockDataTypes, } from './types'; import { formatAccessManagedXBlockData, getIframeUrl, getLegacyEditModalUrl } from './utils'; +import { useUnitSidebarContext } from '../unit-sidebar/UnitSidebarContext'; +import { isUnitPageNewDesignEnabled } from '../utils'; const XBlockContainerIframe: FC = ({ courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit, isUnitVerticalType, }) => { const intl = useIntl(); const dispatch = useDispatch(); + const { setCurrentPageKey } = useUnitSidebarContext(); // Useful to reload iframe const [iframeKey, setIframeKey] = useState(0); @@ -169,8 +172,13 @@ const XBlockContainerIframe: FC = ({ }; const handleOpenManageTagsModal = (id: string) => { - setConfigureXBlockId(id); - openManageTagsModal(); + if (isUnitPageNewDesignEnabled()) { + setCurrentPageKey('align', id); + } else { + // Legacy manage tags modal + setConfigureXBlockId(id); + openManageTagsModal(); + } }; const handleShowProcessingNotification = (variant: string) => { diff --git a/src/generic/sidebar/AlignSidebar.tsx b/src/generic/sidebar/AlignSidebar.tsx new file mode 100644 index 0000000000..2ab724a22f --- /dev/null +++ b/src/generic/sidebar/AlignSidebar.tsx @@ -0,0 +1,25 @@ +import { ContentTagsDrawer } from "@src/content-tags-drawer"; +import { SidebarTitle } from "./SidebarTitle"; +import { SchoolOutline } from "@openedx/paragon/icons"; + +export interface AlignSidebarProps { + contentId: string; + title: string; +} + +/** + * Sidebar that renders Align Sidebar (manage tags sidebar) + * for the given content. + */ +export const AlignSidebar = ({ contentId, title }) => ( +
+ + +
+); From 1654772fa38ccec692eeec0f811513721abb1849 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 30 Jan 2026 21:08:05 -0500 Subject: [PATCH 2/2] test: Adding test for Align sidebar in unit page --- .../OutlineAlignSidebar.test.tsx | 4 +-- .../outline-sidebar/OutlineAlignSidebar.tsx | 2 +- src/course-unit/CourseUnit.test.jsx | 16 +++++++++ .../unit-sidebar/UnitAlignSidebar.test.tsx | 33 +++++++++++++++++++ .../unit-sidebar/UnitAlignSidebar.tsx | 10 +++--- src/course-unit/unit-sidebar/sidebarPages.ts | 2 +- .../xblock-container-iframe/index.tsx | 2 +- src/generic/sidebar/AlignSidebar.tsx | 8 ++--- 8 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 src/course-unit/unit-sidebar/UnitAlignSidebar.test.tsx diff --git a/src/course-outline/outline-sidebar/OutlineAlignSidebar.test.tsx b/src/course-outline/outline-sidebar/OutlineAlignSidebar.test.tsx index 1c1b133206..a0df36ad10 100644 --- a/src/course-outline/outline-sidebar/OutlineAlignSidebar.test.tsx +++ b/src/course-outline/outline-sidebar/OutlineAlignSidebar.test.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render, screen, initializeMocks } from '@src/testUtils'; import * as CourseAuthoringContext from '@src/CourseAuthoringContext'; import * as CourseDetailsApi from '@src/data/apiHooks'; @@ -17,6 +16,7 @@ jest.mock('@src/content-tags-drawer', () => ({ describe('OutlineAlignSidebar', () => { beforeEach(() => { + initializeMocks(); jest .spyOn(CourseAuthoringContext, 'useCourseAuthoringContext') .mockReturnValue({ diff --git a/src/course-outline/outline-sidebar/OutlineAlignSidebar.tsx b/src/course-outline/outline-sidebar/OutlineAlignSidebar.tsx index 23b723f7d4..d8ee533a86 100644 --- a/src/course-outline/outline-sidebar/OutlineAlignSidebar.tsx +++ b/src/course-outline/outline-sidebar/OutlineAlignSidebar.tsx @@ -1,8 +1,8 @@ import { useContentData } from '@src/content-tags-drawer/data/apiHooks'; import { useCourseAuthoringContext } from '@src/CourseAuthoringContext'; import { useCourseDetails } from '@src/data/apiHooks'; -import { useOutlineSidebarContext } from './OutlineSidebarContext'; import { AlignSidebar } from '@src/generic/sidebar/AlignSidebar'; +import { useOutlineSidebarContext } from './OutlineSidebarContext'; /** * Align sidebar for course or selected containers. diff --git a/src/course-unit/CourseUnit.test.jsx b/src/course-unit/CourseUnit.test.jsx index 377a8197d1..4c881e7cb5 100644 --- a/src/course-unit/CourseUnit.test.jsx +++ b/src/course-unit/CourseUnit.test.jsx @@ -2836,4 +2836,20 @@ describe('', () => { render(); 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(); + + await screen.findByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); + + simulatePostMessageEvent(messageTypes.openManageTags, { contentId: blockId }); + + await screen.findByText('Align'); + }); }); diff --git a/src/course-unit/unit-sidebar/UnitAlignSidebar.test.tsx b/src/course-unit/unit-sidebar/UnitAlignSidebar.test.tsx new file mode 100644 index 0000000000..83f63b6af0 --- /dev/null +++ b/src/course-unit/unit-sidebar/UnitAlignSidebar.test.tsx @@ -0,0 +1,33 @@ +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 }) => ( +
+ drawer-mock-{id}-{variant} +
+ )), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ blockId: 'unit-id-1' }), +})); + +describe('OutlineAlignSidebar', () => { + beforeEach(() => { + initializeMocks(); + }); + + it('renders ContentTagsDrawer with the correct id and variant', () => { + render(); + + const drawer = screen.getByTestId('content-tags-drawer'); + + expect(drawer).toBeInTheDocument(); + expect(drawer).toHaveTextContent( + 'drawer-mock-unit-id-1-component', + ); + }); +}); diff --git a/src/course-unit/unit-sidebar/UnitAlignSidebar.tsx b/src/course-unit/unit-sidebar/UnitAlignSidebar.tsx index 5364ccfc1e..6d4448e7fd 100644 --- a/src/course-unit/unit-sidebar/UnitAlignSidebar.tsx +++ b/src/course-unit/unit-sidebar/UnitAlignSidebar.tsx @@ -1,7 +1,7 @@ -import { useParams } from "react-router-dom"; -import { useContentData } from "@src/content-tags-drawer/data/apiHooks"; -import { AlignSidebar } from "@src/generic/sidebar/AlignSidebar"; -import { useUnitSidebarContext } from "./UnitSidebarContext"; +import { useParams } from 'react-router-dom'; +import { useContentData } from '@src/content-tags-drawer/data/apiHooks'; +import { AlignSidebar } from '@src/generic/sidebar/AlignSidebar'; +import { useUnitSidebarContext } from './UnitSidebarContext'; /** * Align sidebar for unit or selected components. @@ -22,7 +22,7 @@ export const UnitAlignSidebar = () => { contentData && 'displayName' in contentData ? contentData.displayName : '' } - contentId={sidebarContentId} + contentId={sidebarContentId || ''} /> ); }; diff --git a/src/course-unit/unit-sidebar/sidebarPages.ts b/src/course-unit/unit-sidebar/sidebarPages.ts index 55f3945743..ee5a431f2c 100644 --- a/src/course-unit/unit-sidebar/sidebarPages.ts +++ b/src/course-unit/unit-sidebar/sidebarPages.ts @@ -31,5 +31,5 @@ export const getUnitSidebarPages = (): UnitSidebarPages => { title: messages.sidebarButtonAlign, }, }), - } + }; }; diff --git a/src/course-unit/xblock-container-iframe/index.tsx b/src/course-unit/xblock-container-iframe/index.tsx index 7532174df1..309ea686b1 100644 --- a/src/course-unit/xblock-container-iframe/index.tsx +++ b/src/course-unit/xblock-container-iframe/index.tsx @@ -178,7 +178,7 @@ const XBlockContainerIframe: FC = ({ // Legacy manage tags modal setConfigureXBlockId(id); openManageTagsModal(); - } + } }; const handleShowProcessingNotification = (variant: string) => { diff --git a/src/generic/sidebar/AlignSidebar.tsx b/src/generic/sidebar/AlignSidebar.tsx index 2ab724a22f..030c9319b7 100644 --- a/src/generic/sidebar/AlignSidebar.tsx +++ b/src/generic/sidebar/AlignSidebar.tsx @@ -1,6 +1,6 @@ -import { ContentTagsDrawer } from "@src/content-tags-drawer"; -import { SidebarTitle } from "./SidebarTitle"; -import { SchoolOutline } from "@openedx/paragon/icons"; +import { ContentTagsDrawer } from '@src/content-tags-drawer'; +import { SchoolOutline } from '@openedx/paragon/icons'; +import { SidebarTitle } from './SidebarTitle'; export interface AlignSidebarProps { contentId: string; @@ -11,7 +11,7 @@ export interface AlignSidebarProps { * Sidebar that renders Align Sidebar (manage tags sidebar) * for the given content. */ -export const AlignSidebar = ({ contentId, title }) => ( +export const AlignSidebar = ({ contentId, title }: AlignSidebarProps) => (